mirror of
https://github.com/lvgl/lvgl.git
synced 2026-05-28 22:30:49 +08:00
chore(static_checks): add scripts to check include guidelines and lv_types (#10094)
This commit is contained in:
@@ -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
|
||||||
@@ -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.
|
||||||
Executable
+733
File diff suppressed because it is too large
Load Diff
Executable
+199
@@ -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()
|
||||||
Reference in New Issue
Block a user