mirror of
https://github.com/lvgl/lvgl.git
synced 2026-05-24 08:16:29 +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