Files
lvgl/scripts/generate_examples.py
T
Gabor Kiss-VamosiandGitHub 02814ddb90
Arduino Lint / lint (push) Has been cancelled
Build Examples with C++ Compiler / build-examples (push) Has been cancelled
MicroPython CI / Build esp32 port (push) Has been cancelled
MicroPython CI / Build rp2 port (push) Has been cancelled
MicroPython CI / Build stm32 port (push) Has been cancelled
MicroPython CI / Build unix port (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_NORMAL_8BIT - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_SDL - Ubuntu (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_16BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_24BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - cl - Windows (push) Has been cancelled
C/C++ CI / Build OPTIONS_FULL_32BIT - gcc - Windows (push) Has been cancelled
C/C++ CI / Build ESP IDF ESP32S3 (push) Has been cancelled
C/C++ CI / Run tests with 32bit build (push) Has been cancelled
C/C++ CI / Run tests with 64bit build (push) Has been cancelled
BOM Check / bom-check (push) Has been cancelled
Verify that lv_conf_internal.h matches repository state / verify-conf-internal (push) Has been cancelled
Verify GDB constants are up-to-date / verify-gdb-consts (push) Has been cancelled
Verify the widget property name / verify-property-name (push) Has been cancelled
Verify code formatting / verify-formatting (push) Has been cancelled
Compare file templates with file names / template-check (push) Has been cancelled
Build Docs / build-and-deploy (push) Has been cancelled
Build .deb packages / build (push) Has been cancelled
Test API JSON generator / Test API JSON (push) Has been cancelled
Install LVGL using CMake / build-examples (private) (push) Has been cancelled
Install LVGL using CMake / build-examples (public) (push) Has been cancelled
Check Makefile / Build using Makefile (push) Has been cancelled
Check Makefile for UEFI / Build using Makefile for UEFI (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/benchmark_results_comment/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/filter_docker_logs/test.sh) (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Script Check (scripts/perf/tests/serialize_results/test.sh) (push) Has been cancelled
Hardware Performance Test / Hardware Performance Benchmark (push) Has been cancelled
Hardware Performance Test / HW Benchmark - Save PR Number (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_32B - Ubuntu (push) Has been cancelled
Performance Tests CI / Perf Tests OPTIONS_TEST_PERF_64B - Ubuntu (push) Has been cancelled
Port repo release update / run-release-branch-updater (push) Has been cancelled
Static Checks / Static Checks (push) Has been cancelled
Verify Font License / verify-font-license (push) Has been cancelled
Verify Kconfig / verify-kconfig (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 32b - lv_conf_perf32b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark 64b - lv_conf_perf64b (push) Has been cancelled
Emulated Performance Test / ARM Emulated Benchmark - Save PR Number (push) Has been cancelled
docs(example): fine-tune examples and flatten directories (#10186)
2026-05-28 10:40:10 +02:00

414 lines
15 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate C files for every XML example by driving `lved-cli.js`.
Each XML example under `examples/` is run through the LVGL Pro editor's CLI
(`lved-cli.js generate <project>`), which only operates on a project tree, so
this script shuttles each example XML through `examples/xml_project/screens/`,
copies the generated `_gen.c` back beside its source XML, and finishes by
running the `cleanup_examples.py` transformations + wrapping the file in an
`#if LV_USE_<topic> && LV_BUILD_EXAMPLES` build guard.
Once every XML is processed the script rewrites each touched topic's
`lv_example_<topic>.h` (one prototype per `.c` on disk), runs
`code-format.py examples` to astyle the output, and wipes every `.c`/`.h`
file from `examples/xml_project/` so the scaffolding the CLI generates can't
collide with the host project's source globbing.
USAGE
-----
python scripts/generate_examples.py [--cli /path/to/lved-cli.js] [paths...]
Without paths, every XML under `examples/` is processed except those inside
`examples/xml_project/` and the top-level `project.xml` / `globals.xml`.
Paths may be individual XML files or directories to recurse into.
"""
from __future__ import annotations
import argparse
import re
import shutil
import subprocess
import sys
from pathlib import Path
# Reuse the transformation pipeline from the sibling cleanup script instead
# of shelling out — calling `process()` directly is faster and avoids a full
# walk of `examples/` on every generated file.
sys.path.insert(0, str(Path(__file__).resolve().parent))
import cleanup_examples # noqa: E402
REPO_ROOT = Path(__file__).resolve().parent.parent
EXAMPLES_DIR = REPO_ROOT / "examples"
PROJECT_DIR = EXAMPLES_DIR / "xml_project"
SCREENS_DIR = PROJECT_DIR / "screens"
CODE_FORMATTER = REPO_ROOT / "scripts" / "code-format.py"
# Files at the examples/ root that are project metadata, not examples.
ROOT_METADATA = {"project.xml", "globals.xml"}
# Topic folders whose name has no matching `LV_USE_<TOPIC>` macro. LVGL
# always builds scroll handling, the style system, and the base `lv_obj`
# widget in, so these examples get gated on `LV_BUILD_EXAMPLES` alone —
# emitting `LV_USE_STYLES` / `LV_USE_SCROLL` / `LV_USE_OBJ` (undefined)
# would make the `#if` false and silently drop every example in those
# folders.
TOPICS_WITHOUT_LV_USE = {"scroll", "styles", "obj"}
# Categories that group several independent topics, each with its own
# `LV_USE_<TOPIC>` macro and `lv_example_<topic>.h` header. For these the
# topic folder is `examples/<category>/<topic>`. Every other category is
# itself a single topic (e.g. `scroll`, `styles`), so its topic folder is
# `examples/<category>`.
#
# Widget examples are flat (`widgets/<topic>/lv_example_*.xml`); `layouts`
# and `libs` still nest a feature folder
# (`<category>/<topic>/<feature>/lv_example_*.xml`). Resolving the topic by
# category rather than by directory depth handles both layouts uniformly.
MULTI_TOPIC_CATEGORIES = {"widgets", "layouts", "libs"}
def find_example_xmls() -> list[Path]:
"""Return every example XML under `examples/` worth processing.
Excludes the working project tree (`examples/xml_project/`) and the
top-level metadata XMLs.
"""
targets = []
for xml in EXAMPLES_DIR.rglob("*.xml"):
if PROJECT_DIR in xml.parents:
continue
if xml.parent == EXAMPLES_DIR and xml.name in ROOT_METADATA:
continue
targets.append(xml)
return sorted(targets)
def resolve_cli(cli_arg: str | None) -> str:
"""Resolve the lved-cli.js path. Use --cli if given, else search PATH."""
if cli_arg:
p = Path(cli_arg).expanduser().resolve()
if not p.exists():
sys.exit(f"--cli path does not exist: {p}")
return str(p)
found = shutil.which("lved-cli.js")
if found:
return found
sys.exit("lved-cli.js not found on PATH; pass --cli /path/to/lved-cli.js")
def _run_generator(cli_path: str) -> subprocess.CompletedProcess:
"""Run `lved-cli.js generate` against the working project tree."""
return subprocess.run(
["node", cli_path, "generate", str(PROJECT_DIR.relative_to(REPO_ROOT))],
cwd=REPO_ROOT,
capture_output=True,
text=True,
)
def generate_one(xml_path: Path, cli_path: str) -> bool:
"""Generate the C file for a single XML. Return True on success."""
name = xml_path.stem # e.g. "lv_example_flex_grow"
staged_xml = SCREENS_DIR / xml_path.name
generated_c = SCREENS_DIR / f"{name}_gen.c"
generated_h = SCREENS_DIR / f"{name}_gen.h"
target_c = xml_path.with_suffix(".c")
SCREENS_DIR.mkdir(parents=True, exist_ok=True)
shutil.copyfile(xml_path, staged_xml)
try:
result = _run_generator(cli_path)
if result.returncode != 0:
sys.stderr.write(
f" ! lved-cli failed (exit {result.returncode}):\n{result.stderr}"
)
return False
if not generated_c.exists():
sys.stderr.write(
f" ! expected {generated_c.name} after generation, not found\n"
)
return False
shutil.copyfile(generated_c, target_c)
cleanup_examples.process(target_c)
wrap_with_build_guard(xml_path, target_c)
return True
finally:
# Tidy up so the screens folder doesn't accumulate per-file artifacts
# between runs. The pre-existing test example (e.g.
# `lv_example_dropdown_text.xml`) is untouched because its name
# differs from whatever we staged.
for f in (staged_xml, generated_c, generated_h):
if f.exists():
f.unlink()
def _topic_dir_for(xml: Path) -> Path:
"""Return the topic folder for an example XML.
The topic folder owns the shared `lv_example_<topic>.h` header and maps
to the `LV_USE_<TOPIC>` build guard. For a multi-topic category
(`widgets`/`layouts`/`libs`) it is `examples/<category>/<topic>`;
otherwise the category folder itself is the topic. Computed from the
category, not the directory depth, so flat widget examples
(`widgets/<topic>/lv_example_*.xml`) and still-nested layouts/libs
(`<category>/<topic>/<feature>/...`) both resolve correctly.
"""
if EXAMPLES_DIR not in xml.parents:
return xml.parent
parts = xml.relative_to(EXAMPLES_DIR).parts
if parts[0] in MULTI_TOPIC_CATEGORIES and len(parts) >= 3:
return EXAMPLES_DIR / parts[0] / parts[1]
return EXAMPLES_DIR / parts[0]
def _guard_expr_for(xml_path: Path) -> str:
"""Return the `#if` expression for an example, derived from its topic.
The topic folder name maps to `LV_USE_<TOPIC>` (e.g. `canvas` →
`LV_USE_CANVAS`, `flex` → `LV_USE_FLEX`). Topics in
`TOPICS_WITHOUT_LV_USE` have no such macro and are gated on
`LV_BUILD_EXAMPLES` only.
"""
topic = _topic_dir_for(xml_path).name
if topic in TOPICS_WITHOUT_LV_USE:
return "LV_BUILD_EXAMPLES"
return f"LV_USE_{topic.upper()} && LV_BUILD_EXAMPLES"
# First `#include` line in the cleaned file. `cleanup_examples` collapses the
# include block to a single relative `lvgl.h`, so the first match is it; the
# `#if` is inserted right after so the guard wraps everything below the
# include (mirroring the hand-written examples).
_FIRST_INCLUDE_RE = re.compile(
r"^#include[ \t]+[\"<][^\">\n]+[\">][ \t]*\n", re.MULTILINE
)
# Idempotency probe: an existing top-level guard already mentioning
# LV_BUILD_EXAMPLES means this file is already wrapped.
_EXISTING_GUARD_RE = re.compile(r"^#if\b.*\bLV_BUILD_EXAMPLES\b", re.MULTILINE)
def wrap_with_build_guard(xml_path: Path, c_path: Path) -> None:
"""Wrap a cleaned example C file in its `#if … #endif` build guard.
Inserts `#if <expr>` directly after the include line and appends
`#endif` at EOF. Idempotent: a file that already carries a
`LV_BUILD_EXAMPLES` guard is left untouched.
"""
text = c_path.read_text()
if _EXISTING_GUARD_RE.search(text):
return
m = _FIRST_INCLUDE_RE.search(text)
if not m:
sys.stderr.write(
f" ! no #include found in {c_path.name}, skipping build guard\n"
)
return
guard = f"#if {_guard_expr_for(xml_path)}\n"
text = text[: m.end()] + guard + text[m.end():]
c_path.write_text(text.rstrip() + "\n#endif\n")
# The per-widget header is rewritten wholesale on every run. Every prototype
# comes from a `.c` file actually present in the widget's folder — there is
# no marker / preserved-region machinery.
#
# Convention used to map `.c` file → prototype. Both forms map to
# `void lv_example_<stem>(void)`:
# * `<topic>/lv_example_<stem>.c` → directly in the topic folder
# (every widget example, plus C-only examples anywhere).
# * `<topic>/<feature>/lv_example_<stem>.c` → still-nested layouts/libs.
HEADER_TEMPLATE = """\
/**
* @file {header_name}
*
*/
#ifndef {guard}
#define {guard}
#ifdef __cplusplus
extern "C" {{
#endif
{prototypes}
#ifdef __cplusplus
}} /*extern "C"*/
#endif
#endif /*{guard}*/
"""
def _collect_prototypes(topic_dir: Path) -> list[str]:
"""Return prototype lines for every `.c` example under `topic_dir`.
Examples sit either directly in the topic folder (every widget example
and any C-only example) or in a feature subfolder (still-nested
layouts/libs). Both globs are unioned so the header lists them all,
each using the bare file stem as the function name (`void <stem>(void)`).
"""
nested_stems = {c.stem for c in topic_dir.glob("*/lv_example_*.c")}
direct_stems = {c.stem for c in topic_dir.glob("lv_example_*.c")}
return [f"void {stem}(void);" for stem in sorted(nested_stems | direct_stems)]
def write_topic_headers(touched: list[Path]) -> list[Path]:
"""Rewrite `lv_example_<topic>.h` from the `.c` files on disk.
No markers, no preserved region. Adding or removing a `.c` file
(XML- or hand-written) automatically reshapes the header on the next
run.
"""
topic_dirs = {_topic_dir_for(xml) for xml in touched}
written: list[Path] = []
for topic_dir in sorted(topic_dirs):
if not topic_dir.is_dir() or EXAMPLES_DIR not in topic_dir.parents:
continue
topic = topic_dir.name
prototypes = _collect_prototypes(topic_dir)
if not prototypes:
continue
header_path = topic_dir / f"lv_example_{topic}.h"
guard = f"LV_EXAMPLE_{topic.upper()}_H"
header_path.write_text(
HEADER_TEMPLATE.format(
header_name=header_path.name,
guard=guard,
prototypes="\n".join(prototypes),
)
)
written.append(header_path)
return written
def cleanup_project_files() -> None:
"""Wipe every `.c`/`.h` file under `examples/xml_project/`.
The CLI generates scaffolding (`xml_project.{c,h}`, `xml_project_gen.{c,h}`,
per-screen `_gen.{c,h}`, plus font/image `_data.c` files) that would collide
with the host project's source globbing. We don't need it between runs —
the CLI re-creates whatever it needs on next invocation.
"""
if not PROJECT_DIR.is_dir():
return
for pattern in ("*.c", "*.h"):
for f in PROJECT_DIR.rglob(pattern):
f.unlink()
def format_examples() -> None:
"""Run the project's astyle wrapper on the examples tree."""
if not CODE_FORMATTER.exists():
sys.stderr.write(
f" ! formatter not found at {CODE_FORMATTER}, skipping format step\n"
)
return
subprocess.run(
[sys.executable, str(CODE_FORMATTER), "examples"],
cwd=REPO_ROOT,
)
def regenerate_project(cli_path: str) -> bool:
"""Re-run the CLI against the now-empty `screens/` to sync project files.
`generate_one` deletes each staged screen in its `finally` block, so once
every example is processed `screens/` is empty again. The project-level
generated artifacts (`xml_project_gen.{c,h}`, `file_list_gen.cmake`) still
reference whatever screen was staged last, though. One final generation
against the empty tree rewrites them to match real project content instead
of a transient staged screen.
Skipped (returning False) if `screens/` still holds staged XML, so a
partial/interrupted run won't bake a half-state into the project files.
"""
staged = sorted(SCREENS_DIR.glob("*.xml"))
if staged:
sys.stderr.write(
" ! screens/ still has XML, skipping final regeneration: "
f"{', '.join(p.name for p in staged)}\n"
)
return False
result = _run_generator(cli_path)
if result.returncode != 0:
sys.stderr.write(
f" ! final regeneration failed (exit {result.returncode}):\n"
f"{result.stderr}"
)
return False
return True
def collect_targets(paths: list[str]) -> list[Path]:
"""Expand user-supplied paths into a flat list of XML files."""
out: list[Path] = []
for raw in paths:
p = Path(raw).resolve()
if p.is_dir():
out.extend(sorted(p.rglob("*.xml")))
elif p.suffix == ".xml" and p.is_file():
out.append(p)
else:
sys.stderr.write(f"skipping (not an XML or directory): {p}\n")
return out
def main(argv: list[str]) -> int:
parser = argparse.ArgumentParser(
description="Generate C files for XML examples via lved-cli.js."
)
parser.add_argument(
"--cli",
help="path to lved-cli.js (defaults to a PATH lookup)",
)
parser.add_argument(
"paths",
nargs="*",
help="specific XML files or directories to process; defaults to every "
"example under examples/",
)
args = parser.parse_args(argv)
cli_path = resolve_cli(args.cli)
targets = collect_targets(args.paths) if args.paths else find_example_xmls()
if not targets:
print("No XML examples found.")
return 0
# `finally` guarantees the scaffolding cleanup runs even if generation
# raises, so the tree never keeps globbable xml_project `.c`/`.h` behind.
ok = 0
try:
for xml in targets:
rel = xml.relative_to(REPO_ROOT) if REPO_ROOT in xml.parents else xml
print(f"processing: {rel}")
if generate_one(xml, cli_path):
ok += 1
headers = write_topic_headers(targets)
for h in headers:
rel = h.relative_to(REPO_ROOT) if REPO_ROOT in h.parents else h
print(f"wrote header: {rel}")
print("regenerating project against empty screens/ to sync _gen files")
regenerate_project(cli_path)
print("formatting examples/ via scripts/code-format.py")
format_examples()
finally:
cleanup_project_files()
print(f"\n{ok} of {len(targets)} examples generated.")
return 0 if ok == len(targets) else 1
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))