Files
lvgl/scripts/generators/config_headers/parse.py
T
André CostaandGitHub 94913bf623
Arduino Lint / lint (push) Canceled after 0s
Build Examples with C++ Compiler / build-examples (push) Canceled after 0s
MicroPython CI / Build esp32 port (push) Canceled after 0s
MicroPython CI / Build rp2 port (push) Canceled after 0s
MicroPython CI / Build stm32 port (push) Canceled after 0s
MicroPython CI / Build unix port (push) Canceled after 0s
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Canceled after 0s
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Canceled after 0s
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Canceled after 0s
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Canceled after 0s
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Canceled after 0s
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Canceled after 0s
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Canceled after 0s
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Canceled after 0s
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Canceled after 0s
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Canceled after 0s
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Canceled after 0s
C/C++ CI / Build ESP IDF ESP32S3 (push) Canceled after 0s
C/C++ CI / Run tests with 32bit build (push) Canceled after 0s
C/C++ CI / Run tests with 64bit build (push) Canceled after 0s
BOM Check / bom-check (push) Canceled after 0s
Verify GDB constants are up-to-date / verify-gdb-consts (push) Canceled after 0s
Verify the widget property name / verify-property-name (push) Canceled after 0s
Verify code formatting / verify-formatting (push) Canceled after 0s
Compare file templates with file names / template-check (push) Canceled after 0s
Code Generation / Code Generation (push) Canceled after 0s
Build Docs / build-and-deploy (push) Canceled after 0s
Build .deb packages / build (push) Canceled after 0s
Validate pkg-config and CMake config / cmake (linux) (push) Canceled after 0s
Validate pkg-config and CMake config / pkgconfig (linux) (push) Canceled after 0s
Validate pkg-config and CMake config / cmake (linux-3d) (push) Canceled after 0s
Validate pkg-config and CMake config / pkgconfig (linux-3d) (push) Canceled after 0s
Test API JSON generator / Test API JSON (push) Canceled after 0s
Install LVGL using CMake / build-examples (private) (push) Canceled after 0s
Install LVGL using CMake / build-examples (public) (push) Canceled after 0s
Check Makefile / Build using Makefile (push) Canceled after 0s
Check Makefile for UEFI / Build using Makefile for UEFI (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Canceled after 0s
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Canceled after 0s
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Canceled after 0s
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Canceled after 0s
Port repo release update / run-release-branch-updater (push) Canceled after 0s
Static Checks / Static Checks (push) Canceled after 0s
Verify Font License / verify-font-license (push) Canceled after 0s
Verify Kconfig / verify-kconfig (push) Canceled after 0s
Hardware Performance Test / Hardware Performance Benchmark (push) Canceled after 0s
Hardware Performance Test / HW Benchmark - Save PR Number (push) Canceled after 0s
build(kconfig): overhaul help and prompt texts for kconfig (#10392)
2026-08-01 09:36:39 +02:00

421 lines
16 KiB
Python

"""Phase 1: turn a Kconfig tree into a list of typed :class:`ConfigEntry`.
All classification logic lives here. ``classify(node)`` is the single place
that decides *what kind* of config a node is; ``parse(path)`` walks the tree
and returns the resulting entries in Kconfig order.
Only the types implemented so far are recognised; ``classify`` returns ``None``
for anything not yet handled, so the model can grow one type at a time without
silently mis-emitting the rest.
"""
from __future__ import annotations
import os
import re
from kconfiglib import BOOL, HEX, INT, STRING, TRISTATE, Choice, Kconfig, Symbol
from .config_entry import (
BoolConfig,
ConfigEntry,
ConstToken,
DerivedFlag,
EnumChoice,
EnumMember,
IntConfig,
StringConfig,
)
from .kconfig_utils import (
choice_default,
choice_int_value,
dep_terms,
doc_text,
int_const_value,
is_int_const,
member_requires,
rev_dep_c_expr,
select_targets,
term_key,
)
# ----------------------------------------------------------------------------
# Enum-choice classification policy
# ----------------------------------------------------------------------------
# Some choices use literal values e.g. (`default 1 if LV_OS_PTHREAD`)
# but they must emit the member name as the token and not the number
# (#define LV_USE_OS LV_OS_PTHREAD instead of #define LV_USE_OS 1)
# These follow the same structure as LV_COLOR_DEPTH where LV_COLOR_DEPTH is exported
# as the raw number (#define LV_COLOR_DEPTH 16)
#
# TODO:(v10) migrate these to the stdlib shape - reference named int-const token
# configs (`config LV_OS_PTHREAD / int / default 1`) instead of bare literals,
# after which the named-const reference IS the signal and this set can be
# deleted. Deferred to the v10 major because freeing the token name requires
# renaming the selector members (CONFIG_LV_OS_PTHREAD -> CONFIG_LV_USE_OS_PTHREAD),
# which breaks embedded users' Kconfig configs.
MEMBER_IS_TOKEN: set[str] = {
"LV_USE_OS", # tokens LV_OS_* are defined into lv_conf_internal.h
"LV_LOG_LEVEL", # LV_LOG_LEVEL_* defined into lv_conf_internal.h
"LV_USE_DRAW_SW_ASM", # LV_DRAW_SW_ASM_* defined into lv_conf_internal.h
"LV_USE_NEMA_LIB", # LV_NEMA_LIB_*
"LV_USE_NEMA_HAL", # LV_NEMA_HAL_*
"LV_NANOVG_BACKEND", # LV_NANOVG_BACKEND_*
"LV_WAYLAND_BACKEND", # LV_WAYLAND_BACKEND_* (SHM/EGL/G2D)
"LV_LINUX_DRM_BACKEND", # LV_LINUX_DRM_BACKEND_* (FBDEV/GBM/EGL)
"LV_SDL_BACKEND", # LV_SDL_BACKEND_* (SW/TEXTURE/EGL)
"LV_CHECK_ARG_LOG_MODE", # LV_CHECK_ARG_LOG_MODE_*
"LV_SDL_MOUSEWHEEL_MODE", # LV_SDL_MOUSEWHEEL_MODE_* owned by a C header
"LV_VG_LITE_GPU", # LV_VG_LITE_GPU_* defined into lv_conf_internal.h; the
# VG-Lite options dispatch header compares against these tokens
}
# Symbols that must never appear in the lv_conf_template.h
IGNORE_SYMBOLS: set[str] = {
# Only makes sense when the user is using kconfig
"LV_CONF_SKIP",
# Defined in `lv_version.h`
"LVGL_VERSION_MAJOR",
"LVGL_VERSION_MINOR",
"LVGL_VERSION_PATCH",
}
# Anonymous choices whose members map to tokens that live in LVGL's
# C headers, with member != token. Keyed by the member-name frozenset (the
# choice is anonymous, so there is no choice name to key on); value is
# (emitted macro, {member: token}).
# TODO:(v10) the note on MEMBER_IS_TOKEN also applies here
_ALIGN = (
"TOP_LEFT",
"TOP_MID",
"TOP_RIGHT",
"BOTTOM_LEFT",
"BOTTOM_MID",
"BOTTOM_RIGHT",
"LEFT_MID",
"RIGHT_MID",
"CENTER",
)
_RENDER = ("PARTIAL", "DIRECT", "FULL")
def _prefixed_map(member_prefix: str, token_prefix: str, suffixes) -> dict:
return {f"{member_prefix}{s}": f"{token_prefix}{s}" for s in suffixes}
CHOICE_TOKEN_MAP: dict = {
frozenset(_prefixed_map("LV_SDL_RENDER_MODE_", "", _RENDER)): (
"LV_SDL_RENDER_MODE",
_prefixed_map("LV_SDL_RENDER_MODE_", "LV_DISPLAY_RENDER_MODE_", _RENDER),
),
frozenset(_prefixed_map("LV_X11_RENDER_MODE_", "", _RENDER)): (
"LV_X11_RENDER_MODE",
_prefixed_map("LV_X11_RENDER_MODE_", "LV_DISPLAY_RENDER_MODE_", _RENDER),
),
frozenset(_prefixed_map("LV_LINUX_FBDEV_RENDER_MODE_", "", _RENDER)): (
"LV_LINUX_FBDEV_RENDER_MODE",
_prefixed_map(
"LV_LINUX_FBDEV_RENDER_MODE_", "LV_DISPLAY_RENDER_MODE_", _RENDER
),
),
frozenset(_prefixed_map("LV_PERF_MONITOR_ALIGN_", "", _ALIGN)): (
"LV_USE_PERF_MONITOR_POS",
_prefixed_map("LV_PERF_MONITOR_ALIGN_", "LV_ALIGN_", _ALIGN),
),
frozenset(_prefixed_map("LV_MEM_MONITOR_ALIGN_", "", _ALIGN)): (
"LV_USE_MEM_MONITOR_POS",
_prefixed_map("LV_MEM_MONITOR_ALIGN_", "LV_ALIGN_", _ALIGN),
),
}
def _is_deprecated(node) -> bool:
"""A config formally flagged deprecated - kept out of the template (handled
by the bridge shims). Matches the bracketed ``[DEPRECATED ...]`` prompt
convention or a help text that *opens* with "Deprecated"
"""
prompt = node.prompt[0] if node.prompt else ""
if "[DEPRECATED" in prompt.upper():
return True
return bool(node.help) and node.help.lstrip().startswith("Deprecated")
def load(path: str) -> Kconfig:
"""Parse a Kconfig file into a kconfiglib ``Kconfig`` object."""
os.environ["srctree"] = os.path.dirname(os.path.abspath(path)) or "."
return Kconfig(path, warn_to_stderr=False, suppress_traceback=True)
def _derived_enum(sym):
"""If *sym* is a derived-enum int - every default has the form
``<token> if <member>`` over a single choice - return
``(choice, [(member_sym, value_expr), ...])`` in default order; else None.
Unifies the old ``enum_derived_choice`` (named-const tokens) and
``int_alias_members`` (bare-literal tokens): both are just "one choice
member per default", differing only in what the default *value* is, which
:func:`_build_enum_choice` interprets.
"""
if sym.type not in (INT, HEX) or not sym.defaults:
return None
dd_keys = {term_key(x) for x in dep_terms(sym.direct_dep)}
choice = None
pairs = []
for value, cond in sym.defaults:
rest = [x for x in dep_terms(cond) if term_key(x) not in dd_keys]
if len(rest) != 1:
return None
member = rest[0]
if not (isinstance(member, Symbol) and member.choice is not None):
return None
if choice is None:
choice = member.choice
elif member.choice is not choice:
return None
pairs.append((member, value))
return (choice, pairs) if choice is not None else None
def _member_token(macro: str, member, value):
"""Resolve one derived-enum member to its emitted ``EnumMember``.
* macro is in ``MEMBER_IS_TOKEN`` -> emit the *member* name; value is the
bare literal, defined here (``LV_USE_OS`` -> ``LV_OS_PTHREAD`` = ``1``).
* otherwise -> reference the default symbol by name,
defining nothing. It is either a bare number (value-alias, ``LV_COLOR_DEPTH``
-> ``16``) or a named :class:`ConstToken` already defined in the options
block (stdlib, ``LV_STDLIB_BUILTIN``).
"""
if macro in MEMBER_IS_TOKEN:
token = member.name
em = EnumMember(token, value.name, True, member.name)
else:
em = EnumMember(value.name, None, False, member.name)
return member, em
def _norm(text: str) -> str:
return re.sub(r"[^a-z0-9]", "", text.lower())
def _enum_doc(choice, resolved) -> str:
"""The choice's help/prompt plus a "Possible values" list, one bullet per
token. A member description is appended only when it adds information: a
bare ``N:`` ordinal is stripped, a ``PREFIX:`` that restates the token is
dropped, and a description that merely echoes the token is omitted."""
cnode = choice.nodes[0]
head = cnode.help or (cnode.prompt[0] if cnode.prompt else "")
lines = [head] if head else []
lines.append("Possible values:")
for member, em in resolved:
prompt = (
member.nodes[0].prompt[0]
if (member.nodes and member.nodes[0].prompt)
else ""
)
desc = re.sub(r"^\s*\d+\s*:\s*", "", prompt).strip()
pre = re.match(r"^([^:]+):\s*(.*)$", desc)
if pre and _norm(pre.group(1)) in _norm(em.token):
desc = pre.group(2).strip()
if not (
desc
and _norm(desc) not in _norm(em.token)
and _norm(em.token) not in _norm(desc)
):
desc = ""
notes = []
requires = member_requires(member, choice)
if requires:
notes.append(f"requires {requires}")
enables = select_targets(member)
if enables:
notes.append("enable: " + ", ".join(enables))
suffix = f" ({'; '.join(notes)})" if notes else ""
bullet = f"- {em.token}: {desc}" if desc else f"- {em.token}"
lines.append(bullet + suffix)
return "\n".join(lines)
def _assemble_enum(
macro, choice, node, resolved, *, needs_bridge, selected=None, doc=None
) -> EnumChoice:
"""Build an :class:`EnumChoice` from resolved ``(member, EnumMember)`` pairs.
Shared by the derived-int path (:func:`_build_enum_choice`) and the bare
-choice path (:func:`_build_choice`). *selected* overrides the emitted
token (value-alias choices pass a gating-aware resolved number); *doc*
overrides the comment (value-aliases use the int symbol's own help and get
no "Possible values" list)."""
if selected is not None:
selected_token = selected
else:
sel = choice_default(choice)
selected_token = next(
(em.token for member, em in resolved if member is sel),
resolved[0][1].token,
)
cnode = choice.nodes[0]
return EnumChoice(
macro,
[em for _, em in resolved],
selected_token,
options_title=cnode.prompt[0] if cnode.prompt else "",
needs_bridge=needs_bridge,
node=node,
doc=_enum_doc(choice, resolved) if doc is None else doc,
)
def _build_enum_choice(sym, node) -> EnumChoice | None:
"""Bucket: a choice backed by a derived int (stdlib, OS, COLOR_DEPTH...)."""
derived = _derived_enum(sym)
if derived is None:
return None
choice, pairs = derived
resolved = [_member_token(sym.name, member, value) for member, value in pairs]
# A value-alias (members map to bare numbers, e.g. LV_COLOR_DEPTH or the
# buffer counts) emits the selected member's number and documents itself
# with the int symbol's own help (no "Possible values" list, which only fits
# symbolic tokens).
value_alias = sym.name not in MEMBER_IS_TOKEN and not any(
isinstance(v, Symbol) and is_int_const(v) for _, v in pairs
)
selected = choice_int_value(sym, choice, pairs) if value_alias else None
doc = doc_text(node) if value_alias else None
return _assemble_enum(
sym.name, choice, node, resolved, needs_bridge=False, selected=selected, doc=doc
)
def _build_choice(choice, node) -> EnumChoice | None: # | BoolGroupChoice :
"""A choice with NO derived int.
* member set in ``CHOICE_TOKEN_MAP`` -> single macro, mapped tokens
(render modes, monitor positions).
* named choice -> single macro, member == token (B1:
LV_TXT_ENC, LV_BIDI_BASE_DIR_DEF).
Bare-choice tokens always live in C headers, so none are (re)defined."""
members = [m for m in choice.syms if m.name not in IGNORE_SYMBOLS]
if not members:
return None
if _is_deprecated(node):
return None
member_names = frozenset(m.name for m in members)
override = CHOICE_TOKEN_MAP.get(member_names)
if override:
macro, tokenmap = override
elif choice.name:
macro, tokenmap = choice.name, None
else:
raise ValueError(
f"Invalid choice config:\n{choice}\n Use a standardized choice instead"
)
resolved = []
for member in members:
token = tokenmap.get(member.name, member.name) if tokenmap else member.name
resolved.append((member, EnumMember(token, None, False, member.name)))
return _assemble_enum(macro, choice, node, resolved, needs_bridge=True)
def enum_backed_choices(kconf: Kconfig) -> set:
"""The set of ``Choice`` objects represented by a derived-enum int (stdlib,
LV_USE_OS, LV_COLOR_DEPTH, LV_LOG_LEVEL ...). These are emitted via their
int (as :class:`EnumChoice`); the choice node itself is skipped, so it is
*not* mistaken for an anonymous :class:`BoolGroupChoice`."""
out = set()
for sym in kconf.unique_defined_syms:
derived = _derived_enum(sym)
if derived is not None:
out.add(derived[0])
return out
def classify(node, enum_choices: frozenset = frozenset()) -> ConfigEntry | None:
"""Map a single Kconfig ``MenuNode`` to a typed ``ConfigEntry``.
*enum_choices* is the set from :func:`enum_backed_choices`; a choice in it
is emitted via its derived int, so the choice node itself is skipped.
Returns ``None`` for nodes that are not (yet) a recognised config kind.
"""
item = node.item
if isinstance(item, Choice):
if item in enum_choices:
return None # handled via its derived int (EnumChoice)
return _build_choice(item, node)
if not isinstance(item, Symbol):
return None
# Choice members are owned by their choice, not classified standalone.
if item.choice is not None:
return None
# Build-system-only / internally-derived / specially-handled symbols.
if item.name in IGNORE_SYMBOLS:
return None
# Deprecated options are kept out of the template.
if _is_deprecated(node):
return None
# Derived-enum ints carry no prompt of their own (the prompt is on the
# selector choice), so this must run before the prompt gate below.
enum = _build_enum_choice(item, node)
if enum is not None:
return enum
# Internal capability flags are no-prompt bools set via `select`; derived
# from their reverse-deps. Also runs before the prompt gate. rev_dep_c_expr
# is choice-aware: a flag selected by a choice member (e.g. LV_USE_EGL, set
# by the Wayland EGL backend) carries a bare <choice> term that it drops,
# rather than leaving the flag unclassified and silently dropped.
if item.type in (BOOL, TRISTATE) and not node.prompt:
if item.rev_dep is not item.kconfig.n:
expr = rev_dep_c_expr(item.rev_dep)
if expr is not None:
return DerivedFlag(item.name, expr, node=node)
if item.type in (INT, HEX):
# A promptless int/hex with a single literal default is a named constant
# (LV_STDLIB_BUILTIN -> 0): define it in the options block at the top,
# where the enum choices that reference it by name can see it.
if is_int_const(item):
return ConstToken(item.name, int_const_value(item), node=node)
# Any other int/hex is emitted - including no-prompt computed aliases
# like LV_SDL_BUF_COUNT (the prompt gate below would wrongly drop it).
return IntConfig.from_symbol(item, node)
# Bool/string options require a prompt to be user-facing.
if not node.prompt:
return None
if item.type == BOOL:
return BoolConfig.from_symbol(item, node)
if item.type == STRING:
return StringConfig.from_symbol(item, node)
return None
def parse_entries(kconf: Kconfig) -> list[ConfigEntry]:
"""Classify every node of an already-loaded *kconf*, in tree order."""
enum_choices = enum_backed_choices(kconf)
entries: list[ConfigEntry] = []
seen: set[str] = set()
def walk(node):
while node:
entry = classify(node, enum_choices)
if entry is not None and entry.name not in seen:
seen.add(entry.name)
entries.append(entry)
if node.list:
walk(node.list)
node = node.next
walk(kconf.top_node.list)
return entries
def parse(path: str) -> list[ConfigEntry]:
"""Load *path* and return every recognised config entry, in tree order."""
return parse_entries(load(path))