chore(static_checks): add scripts to check include guidelines and lv_types (#10094)

This commit is contained in:
André Costa
2026-05-12 09:36:54 +02:00
committed by GitHub
parent 93bcda9271
commit 5168e53d1f
4 changed files with 1186 additions and 0 deletions
+123
View File
@@ -0,0 +1,123 @@
name: Static Checks
on:
push:
branches: ["**"]
pull_request:
branches: ["**"]
# https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency
# Ensure that only one commit will be running tests at a time on each PR
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
jobs:
static-checks:
name: Static Checks
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# ------------------------------------------------------------------
# Every header in include/lvgl/ must appear in include/lvgl/lvgl.h
# ------------------------------------------------------------------
- name: "Check: public API umbrella (include/lvgl/lvgl.h)"
id: public-api
continue-on-error: true
run: python scripts/static_checks/check_headers.py public-api
# ------------------------------------------------------------------
# Every header in src/ must appear in src/lvgl_private.h
# ------------------------------------------------------------------
- name: "Check: private API umbrella (src/lvgl_private.h)"
id: private-api
continue-on-error: true
run: python scripts/static_checks/check_headers.py private-api
# ------------------------------------------------------------------
# - All #includes inside must be valid relative paths.
# - Angle-bracket includes must be in the explicit allow-list
# - see `ALLOWED_EXTERNAL_HEADERS` in `scripts/static_checks/check_headers.py`
# - forbidden headers (stdint.h etc.) must use LV_*_INCLUDE macros.
# ------------------------------------------------------------------
- name: "Check: include path validity + angle-bracket policy"
id: include-paths
continue-on-error: true
run: python scripts/static_checks/check_headers.py include-paths
# ------------------------------------------------------------------
# All external headers in the allow-list (see check above), are used
# ------------------------------------------------------------------
- name: "Check: all headers in allow-list are used"
id: allowed-list
continue-on-error: true
run: python scripts/static_checks/check_headers.py allowed-list-audit
# ------------------------------------------------------------------
# No file may include a deprecated header
# ------------------------------------------------------------------
- name: "Check: no deprecated header includes"
id: deprecated
continue-on-error: true
run: python scripts/static_checks/check_headers.py deprecated
# ------------------------------------------------------------------
# Every header file inside `src` must use `lvgl_public.h` to access
# the public API
# ------------------------------------------------------------------
- name: "Check: no direct public include from source files"
id: direct-public-include
continue-on-error: true
run: python scripts/static_checks/check_headers.py no-direct-public-include
# ------------------------------------------------------------------
# Every typedef struct/enum in include/lvgl/lv_types.h must have
# a real definition (with a body) somewhere
# ------------------------------------------------------------------
- name: "Check: lv_types.h forward declarations have a real definition"
id: lv_types
continue-on-error: true
run: python scripts/static_checks/check_lv_types.py
# ------------------------------------------------------------------
# Final gate fail the job if any step above failed.
# This gives us full output from every check before failing.
# ------------------------------------------------------------------
- name: "Gate: fail if any check failed"
if: |
steps.public-api.outcome == 'failure' ||
steps.private-api.outcome == 'failure' ||
steps.include-paths.outcome == 'failure' ||
steps.allowed-list.outcome == 'failure' ||
steps.direct-public-include.outcome == 'failure' ||
steps.deprecated.outcome == 'failure' ||
steps.lv_types.outcome == 'failure'
run: |
echo "Policy check results:"
echo "---------------------"
print_result() {
local name="$1"
local outcome="$2"
if [ "$outcome" = "success" ]; then
echo " ✅ $name"
else
echo " ❌ $name"
fi
}
print_result "public-api" "${{ steps.public-api.outcome }}"
print_result "private-api" "${{ steps.private-api.outcome }}"
print_result "include-paths" "${{ steps.include-paths.outcome }}"
print_result "allowed-list" "${{ steps.allowed-list.outcome }}"
print_result "direct-public-include" "${{ steps.direct-public-include.outcome }}"
print_result "deprecated" "${{ steps.deprecated.outcome }}"
print_result "lv_types" "${{ steps.lv_types.outcome }}"
echo "---------------------"
echo "One or more policy checks failed. See above for details."
exit 1
+131
View File
@@ -0,0 +1,131 @@
---
title: Coding Guidelines
description: Code style rules and policies enforced by LVGL's CI pipeline.
---
# Coding Guidelines
This page documents the source-code policies that are enforced automatically on every pull request.
Each rule exists for a concrete reason, the goal is to make LVGL easy to integrate, portable
across toolchains, and maintainable as the codebase grows.
<Callout type="info" title="Automated enforcement">
All policies below are checked by the **Policy Checks** CI workflow. The workflow runs every check
independently and reports all failures before marking a PR as failing, so you will always see the
full picture in one run.
</Callout>
## Include guidelines
### 1. External dependencies must use angle-bracket notation
Headers belonging to external libraries and system-provided headers must use angle-bracket notation
rather than quoted paths.
```c
/* Wrong */
#include "freertos/FreeRTOS.h"
/* Correct */
#include <freertos/FreeRTOS.h>
```
Reason: Angle-bracket notation signals that a header is not part of this project and should be
resolved through the compiler's system or configured include search paths. Quoted includes imply a
path relative to the current file, which is semantically incorrect for external dependencies and
can cause resolution to silently find the wrong file.
<Callout type="info" title="Automated enforcement">
Angle-bracket includes are subject to the allow-list and macro rules described in rule 2 below.
</Callout>
---
### 2. Angle-bracket includes must use the allow-list or `LV_*` macros
Angle-bracket includes are only permitted in two situations:
- The header appears in `ALLOWED_EXTERNAL_HEADERS` in `scripts/static_checks/check_headers.py`.
- The include uses one of the LVGL indirection macros.
The following standard headers are **forbidden** as direct angle-bracket includes.
Use the corresponding macro instead:
| Forbidden | Use instead |
|----------------------------|--------------------------------|
| `#include <stdint.h>` | `#include LV_STDINT_INCLUDE` |
| `#include <stddef.h>` | `#include LV_STDDEF_INCLUDE` |
| `#include <stdbool.h>` | `#include LV_STDBOOL_INCLUDE` |
| `#include <inttypes.h>` | `#include LV_INTTYPES_INCLUDE` |
| `#include <limits.h>` | `#include LV_LIMITS_INCLUDE` |
| `#include <stdarg.h>` | `#include LV_STDARG_INCLUDE` |
Reason: LVGL targets a wide range of embedded toolchains, including bare-metal environments
without a standard C library, or environments where these headers live in non-standard locations.
The `LV_*_INCLUDE` macros can be overridden by the user to point to platform-specific
replacements, giving integrators full control.
The allow-list also serves a documentation purpose: every external dependency is explicitly
enumerated in one place, making it easy to audit what LVGL pulls in from the outside and to
assess the cost of porting to a new platform.
<Callout type="info" title="Adding a new allowed header">
To add a new allowed angle-bracket include, add it to `ALLOWED_EXTERNAL_HEADERS` in
`scripts/static_checks/check_headers.py` with a comment explaining why it is needed.
</Callout>
---
### 3. All `#include` directives inside `src/` and `include/` must use relative paths
Source files inside `src/` must use quoted, relative includes, e.g. `"../core/lv_obj_private.h"`.
Reason: LVGL supports build environments, such as Arduino, where the build system does not
allow specifying additional include search directories. Relative paths work everywhere because they
are resolved by the preprocessor relative to the including file, with no reliance on `-I` flags or
toolchain configuration.
---
### 4. Internal code must not include public headers directly
Source files under `src/` must not include headers from `include/lvgl/` by path. Instead, they
must obtain the full public API through `lvgl_public.h`, which itself pulls in `lvgl.h`.
```c
/* Wrong, do not do this */
#include "../../include/lvgl/lv_obj.h"
/* Correct */
#include "../lvgl_public.h"
```
Reason: Source files under `src/` must not reach into `include/lvgl/` directly because the
relative paths become deeply nested and fragile as the directory structure evolves. More
importantly, `lvgl_public.h` serves as an indirection point that adapts to the installation
context: during development it resolves to `../include/lvgl/lvgl.h`, but once the library is
installed it redirects to the system-installed `../lvgl/lvgl.h`. Bypassing it by including public
headers directly would break this mechanism and cause include resolution to fail in installed
builds.
---
### 5. Every public header must be reachable from `lvgl.h`
Every `.h` file under `include/lvgl/` must be directly included by `include/lvgl/lvgl.h`.
Reason: Users integrating LVGL are not required to use the umbrella header, but if they do,
`#include "lvgl.h"` should be enough to pull in the entire public API. Internally, LVGL's own
source code also uses `lvgl.h` as the single authoritative entry point for the public API. This
keeps maintenance simple and avoids accidental omissions.
---
### 6. Every private header must be reachable from `lvgl_private.h`
Every `.h` file under `src/` must be directly included by `src/lvgl_private.h`.
Reason: The same reasoning applies internally: `lvgl_private.h` is the single entry point for
LVGL's private API. Any internal translation unit that needs access to internal types or
functions includes this one header and gets everything. This makes the internal surface area
auditable and prevents headers from silently falling out of the dependency graph.
File diff suppressed because it is too large Load Diff
+199
View File
@@ -0,0 +1,199 @@
#!/usr/bin/env python3
"""
Check that every forward-declared struct/enum in include/lvgl/lv_types.h
has a real definition (with a body) somewhere inside src/.
Rules:
- Only `typedef struct _Tag` and `typedef enum _Tag` entries are checked.
- A definition is a line containing `struct _Tag {` or `enum _Tag {`.
- The `{` must be on the same line as the tag name.
- lv_types.h itself is never used as evidence of a definition.
Exit codes:
0 all forward declarations have a matching definition
1 one or more forward declarations are dangling
"""
from __future__ import annotations
import subprocess
import itertools
import re
import sys
from pathlib import Path
# ---------------------------------------------------------------------------
# Parsing
# ---------------------------------------------------------------------------
# Matches:
# typedef struct _lv_obj_t lv_obj_t;
# typedef enum _lv_align_t lv_align_t;
FORWARD_DECL_RE = re.compile(
r"^\s*typedef\s+(struct|enum)\s+(\S+)\s+\S+\s*;",
re.MULTILINE,
)
def parse_forward_declarations(lv_types_h: Path) -> list[tuple[str, str]]:
"""
Return [(kind, tag), ...] for every typedef struct/enum in lv_types.h.
kind is 'struct' or 'enum', tag is the backing name e.g. '_lv_obj_t'.
"""
content = lv_types_h.read_text(encoding="utf-8", errors="ignore")
return [
(m.group(1), m.group(2))
for m in FORWARD_DECL_RE.finditer(content)
]
# ---------------------------------------------------------------------------
# Searching
# ---------------------------------------------------------------------------
SOURCE_EXTENSIONS = {".c", ".cpp", ".h", ".hpp"}
def find_definitions_slow(repo_root: Path, kind: str, tag: str) -> list[Path]:
"""
Return all files under src_dir that contain a line like:
struct _lv_obj_t {
enum _lv_align_t {
(whitespace between tag and { is allowed, but both must be on one line)
"""
src_dir = repo_root / "src"
include_dir = repo_root / "include" / "lvgl"
# e.g. struct\s+_lv_obj_t\s*\{
pattern = re.compile(
rf"\b{re.escape(kind)}\s+{re.escape(tag)}\s*\{{"
)
found = []
src_source_files = src_dir.rglob("*")
include_headers = include_dir.rglob("*")
for path in sorted(itertools.chain(src_source_files, include_headers)):
if path.suffix not in SOURCE_EXTENSIONS:
continue
try:
content = path.read_text(encoding="utf-8", errors="ignore")
except OSError:
continue
if pattern.search(content):
found.append(path)
return found
def find_definitions(repo_root: Path, kind: str, tag: str) -> list[Path]:
# Construct the search pattern for grep
# \b ensures we match word boundaries
pattern = rf"\b{kind}\s+{tag}\s*\{{"
# Define the search paths
search_paths = [
str(repo_root / "src"),
str(repo_root / "include" / "lvgl")
]
# Build the grep command:
# -r: recursive
# -l: only print names of files with matches
# -E: use extended regular expressions
# --include: filter for specific extensions
cmd = ["grep", "-rlE", pattern]
for ext in SOURCE_EXTENSIONS:
# Converts ".h" to "*.h" for grep
cmd.append(f"--include=*{ext}")
cmd.extend(search_paths)
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
check=False
)
if result.stdout:
# Split lines and convert back to Path objects
return [Path(p) for p in result.stdout.splitlines()]
except FileNotFoundError:
# Fallback or error if grep isn't installed (e.g., on Windows)
print("Grep not found. Falling back to slow search.")
return find_definitions_slow(repo_root, kind, tag)
return []
# ---------------------------------------------------------------------------
# Main check
# ---------------------------------------------------------------------------
def check_forward_declarations(repo_root: Path) -> bool:
lv_types_h = repo_root / "include" / "lvgl" / "lv_types.h"
if not lv_types_h.exists():
print(f"ERROR: {lv_types_h} not found")
return False
declarations = parse_forward_declarations(lv_types_h)
if not declarations:
print("WARNING: no typedef struct/enum entries found in lv_types.h")
return True
print(f"Found {len(declarations)} forward declarations in lv_types.h")
dangling: list[tuple[str, str]] = []
for kind, tag in declarations:
print(f"{kind} {tag} ->", end= " ")
hits = find_definitions(repo_root, kind, tag)
if not hits:
print(f"KO")
dangling.append((kind, tag))
else:
for h in hits:
print(f"{h.relative_to(repo_root)} OK")
if dangling:
print(
f"DANGLING forward declarations in lv_types.h "
f"({len(dangling)} of {len(declarations)}):\n"
)
for kind, tag in dangling:
print(f" {kind} {tag}")
return False
print(
f"OK: all {len(declarations)} forward declarations in lv_types.h "
f"have a matching definition"
)
return True
# ---------------------------------------------------------------------------
# CLI
# ---------------------------------------------------------------------------
def main() -> None:
import argparse
parser = argparse.ArgumentParser(
description="Check lv_types.h forward declarations against src/ definitions"
)
parser.add_argument(
"--root",
default=".",
help="Repository root (default: current directory)",
)
args = parser.parse_args()
repo_root = Path(args.root).resolve()
passed = check_forward_declarations(repo_root)
sys.exit(0 if passed else 1)
if __name__ == "__main__":
main()