tools: split structure checks by responsibility
This commit is contained in:
parent
5f3472f651
commit
98463ad991
@ -75,7 +75,10 @@
|
|||||||
- 可独立执行的检查放在 `scripts/checks/`。
|
- 可独立执行的检查放在 `scripts/checks/`。
|
||||||
- 多个检查共享的函数放在 `scripts/lib/`。
|
- 多个检查共享的函数放在 `scripts/lib/`。
|
||||||
- 不把所有检查逻辑持续堆进 `scripts/check.sh`。
|
- 不把所有检查逻辑持续堆进 `scripts/check.sh`。
|
||||||
- 项目结构规则放在 `scripts/tools/check_structure.py`,由 `scripts/checks/structure.sh` 调用;优先用宿主 Linux/LLVM 工具实现底层检查,把 Tianole 自己的目录和 include 约束固化在轻量 Python 工具里。
|
- 项目结构规则由 `scripts/tools/check_structure.py` 编排,具体规则拆到
|
||||||
|
`scripts/tools/structure_checks/`;不要把 Python 工具堆成单个超大文件。
|
||||||
|
- 优先用宿主 Linux/LLVM 工具实现底层检查,把 Tianole 自己的目录和 include
|
||||||
|
约束固化在轻量 Python 工具里。
|
||||||
- 启动阶段内核自测集中放在 `kernel/selftest/`,不要散落在具体实现目录里。
|
- 启动阶段内核自测集中放在 `kernel/selftest/`,不要散落在具体实现目录里。
|
||||||
|
|
||||||
## 验证
|
## 验证
|
||||||
|
|||||||
2
scripts/tools/.gitignore
vendored
Normal file
2
scripts/tools/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
@ -1,449 +1,46 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import argparse
|
import argparse
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
from structure_checks.common import (
|
||||||
SOURCE_ROOTS = ("arch", "include", "kernel", "mm")
|
all_files,
|
||||||
C_SOURCE_ROOTS = ("arch", "kernel", "mm")
|
c_files,
|
||||||
MAKEFILE_SOURCE_VARS = (
|
changed_files,
|
||||||
"ARCH_KERNEL_SRCS",
|
public_headers,
|
||||||
"ARCH_MM_SRCS",
|
repo_root,
|
||||||
"BOOT_SRCS",
|
source_files,
|
||||||
"KERNEL_SRCS",
|
)
|
||||||
"MM_SRCS",
|
from structure_checks.makefiles import check_makefile_source_lists
|
||||||
|
from structure_checks.public_headers import check_public_header_docs
|
||||||
|
from structure_checks.source_rules import (
|
||||||
|
check_no_bare_negative_errno,
|
||||||
|
check_no_relative_parent_includes,
|
||||||
|
check_scheduler_state_writes,
|
||||||
|
check_selftests_are_centralized,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def repo_root() -> Path:
|
def parse_args() -> argparse.Namespace:
|
||||||
return Path(__file__).resolve().parents[2]
|
|
||||||
|
|
||||||
|
|
||||||
def run_git(root: Path, args: list[str]) -> list[str]:
|
|
||||||
result = subprocess.run(
|
|
||||||
["git", *args],
|
|
||||||
cwd=root,
|
|
||||||
check=True,
|
|
||||||
text=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
)
|
|
||||||
return [line for line in result.stdout.splitlines() if line]
|
|
||||||
|
|
||||||
|
|
||||||
def changed_files(root: Path) -> set[Path]:
|
|
||||||
files = set()
|
|
||||||
|
|
||||||
for path in run_git(root, ["diff", "--name-only", "--diff-filter=ACMR"]):
|
|
||||||
files.add(Path(path))
|
|
||||||
|
|
||||||
for path in run_git(
|
|
||||||
root, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]
|
|
||||||
):
|
|
||||||
files.add(Path(path))
|
|
||||||
|
|
||||||
for path in run_git(root, ["ls-files", "--others", "--exclude-standard"]):
|
|
||||||
files.add(Path(path))
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
def all_files(root: Path) -> set[Path]:
|
|
||||||
files = set()
|
|
||||||
|
|
||||||
for base in SOURCE_ROOTS:
|
|
||||||
root_dir = root / base
|
|
||||||
if not root_dir.exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
for path in root_dir.rglob("*"):
|
|
||||||
if path.is_file():
|
|
||||||
files.add(path.relative_to(root))
|
|
||||||
|
|
||||||
for path in (root / "scripts").rglob("*"):
|
|
||||||
if path.is_file():
|
|
||||||
files.add(path.relative_to(root))
|
|
||||||
|
|
||||||
return files
|
|
||||||
|
|
||||||
|
|
||||||
def source_files(files: set[Path]) -> list[Path]:
|
|
||||||
return sorted(path for path in files if path.suffix in (".c", ".h"))
|
|
||||||
|
|
||||||
|
|
||||||
def public_headers(files: set[Path]) -> list[Path]:
|
|
||||||
return sorted(
|
|
||||||
path
|
|
||||||
for path in files
|
|
||||||
if path.suffix == ".h"
|
|
||||||
and len(path.parts) == 3
|
|
||||||
and path.parts[:2] == ("include", "tianole")
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def c_files(files: set[Path]) -> list[Path]:
|
|
||||||
return sorted(
|
|
||||||
path
|
|
||||||
for path in files
|
|
||||||
if path.suffix == ".c" and path.parts and path.parts[0] in C_SOURCE_ROOTS
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def read_text(root: Path, path: Path) -> str:
|
|
||||||
return (root / path).read_text(encoding="utf-8")
|
|
||||||
|
|
||||||
|
|
||||||
def check_no_relative_parent_includes(root: Path, files: list[Path]) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
include_re = re.compile(r'^\s*#include\s+[<"]\.\./')
|
|
||||||
|
|
||||||
for path in files:
|
|
||||||
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
|
||||||
if include_re.search(line):
|
|
||||||
errors.append(
|
|
||||||
f"{path}:{line_no}: do not use ../ includes; "
|
|
||||||
"promote shared headers or add a private include root"
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_no_bare_negative_errno(root: Path, files: list[Path]) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
return_re = re.compile(r"\breturn\s+-[0-9]+\s*;")
|
|
||||||
|
|
||||||
for path in files:
|
|
||||||
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
|
||||||
if return_re.search(line):
|
|
||||||
errors.append(
|
|
||||||
f"{path}:{line_no}: return a symbolic negative errno "
|
|
||||||
"such as -EINVAL instead of a bare negative number"
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_scheduler_state_writes(root: Path, files: list[Path]) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
state_write_re = re.compile(r"->state\s*=")
|
|
||||||
allowed_path = Path("kernel/sched/sched.h")
|
|
||||||
|
|
||||||
for path in files:
|
|
||||||
if path == allowed_path:
|
|
||||||
continue
|
|
||||||
|
|
||||||
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
|
||||||
if state_write_re.search(line):
|
|
||||||
errors.append(
|
|
||||||
f"{path}:{line_no}: thread state writes must use "
|
|
||||||
"kernel/sched/sched.h helpers"
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def is_function_declaration_start(line: str) -> bool:
|
|
||||||
stripped = line.strip()
|
|
||||||
|
|
||||||
if stripped == "" or stripped.startswith("#"):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if stripped.startswith(("typedef ", "struct ", "enum ", "union ")):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if "(" not in stripped or stripped.startswith("*"):
|
|
||||||
return False
|
|
||||||
|
|
||||||
if "(*" in stripped or stripped.endswith("{"):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def collect_function_declarations(lines: list[str]) -> list[tuple[int, int, str]]:
|
|
||||||
decls = []
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
while index < len(lines):
|
|
||||||
if not is_function_declaration_start(lines[index]):
|
|
||||||
index += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
start = index
|
|
||||||
text = lines[index].strip()
|
|
||||||
while ";" not in lines[index]:
|
|
||||||
index += 1
|
|
||||||
if index >= len(lines):
|
|
||||||
break
|
|
||||||
text += " " + lines[index].strip()
|
|
||||||
|
|
||||||
if index < len(lines):
|
|
||||||
decls.append((start, index, text))
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
return decls
|
|
||||||
|
|
||||||
|
|
||||||
def is_public_type_start(line: str) -> bool:
|
|
||||||
stripped = line.strip()
|
|
||||||
|
|
||||||
if stripped.startswith(("struct ", "enum ", "union ")):
|
|
||||||
return stripped.endswith("{") or "{" in stripped
|
|
||||||
|
|
||||||
if not stripped.startswith("typedef "):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return "(" not in stripped
|
|
||||||
|
|
||||||
|
|
||||||
def collect_public_type_declarations(lines: list[str]) -> list[tuple[int, int, str]]:
|
|
||||||
decls = []
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
while index < len(lines):
|
|
||||||
if not is_public_type_start(lines[index]):
|
|
||||||
index += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
start = index
|
|
||||||
text = lines[index].strip()
|
|
||||||
while ";" not in lines[index]:
|
|
||||||
index += 1
|
|
||||||
if index >= len(lines):
|
|
||||||
break
|
|
||||||
text += " " + lines[index].strip()
|
|
||||||
|
|
||||||
if index < len(lines):
|
|
||||||
decls.append((start, index, text))
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
return decls
|
|
||||||
|
|
||||||
|
|
||||||
def is_documented_public_define(lines: list[str], index: int, guard: str | None) -> bool:
|
|
||||||
stripped = lines[index].strip()
|
|
||||||
|
|
||||||
if not stripped.startswith("#define "):
|
|
||||||
return False
|
|
||||||
|
|
||||||
parts = stripped.split()
|
|
||||||
if len(parts) < 2:
|
|
||||||
return False
|
|
||||||
|
|
||||||
name = parts[1].split("(", 1)[0]
|
|
||||||
if name == guard:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def header_guard_name(lines: list[str]) -> str | None:
|
|
||||||
for line in lines:
|
|
||||||
stripped = line.strip()
|
|
||||||
if stripped.startswith("#define "):
|
|
||||||
parts = stripped.split()
|
|
||||||
if len(parts) >= 2:
|
|
||||||
return parts[1]
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def has_kernel_doc_before(lines: list[str], start: int) -> bool:
|
|
||||||
index = start - 1
|
|
||||||
|
|
||||||
while index >= 0 and lines[index].strip() == "":
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
if index < 0 or lines[index].strip() != "*/":
|
|
||||||
return False
|
|
||||||
|
|
||||||
while index >= 0:
|
|
||||||
if lines[index].strip().startswith("/**"):
|
|
||||||
return True
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
def has_blank_line_before_doc(lines: list[str], start: int) -> bool:
|
|
||||||
index = start - 1
|
|
||||||
|
|
||||||
while index >= 0 and lines[index].strip() == "":
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
if index < 0 or lines[index].strip() != "*/":
|
|
||||||
return True
|
|
||||||
|
|
||||||
while index >= 0 and not lines[index].strip().startswith("/**"):
|
|
||||||
index -= 1
|
|
||||||
|
|
||||||
if index <= 0:
|
|
||||||
return True
|
|
||||||
|
|
||||||
return lines[index - 1].strip() == ""
|
|
||||||
|
|
||||||
|
|
||||||
def symbol_name_from_type_declaration(text: str) -> str:
|
|
||||||
if text.startswith("typedef "):
|
|
||||||
return text.rsplit("}", 1)[-1].strip().rstrip(";").split()[-1]
|
|
||||||
|
|
||||||
parts = text.split()
|
|
||||||
if len(parts) >= 2:
|
|
||||||
return parts[1]
|
|
||||||
|
|
||||||
return "<unknown>"
|
|
||||||
|
|
||||||
|
|
||||||
def check_kernel_doc_block(
|
|
||||||
errors: list[str], path: Path, lines: list[str], start: int, kind: str, name: str
|
|
||||||
) -> None:
|
|
||||||
line_no = start + 1
|
|
||||||
|
|
||||||
if not has_kernel_doc_before(lines, start):
|
|
||||||
errors.append(
|
|
||||||
f"{path}:{line_no}: public {kind} '{name}' needs "
|
|
||||||
"a kernel-doc comment immediately before it"
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
if not has_blank_line_before_doc(lines, start):
|
|
||||||
errors.append(
|
|
||||||
f"{path}:{line_no}: public {kind} '{name}' "
|
|
||||||
"comment block must be separated by a blank line"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def check_public_header_docs(root: Path, files: list[Path]) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for path in files:
|
|
||||||
lines = read_text(root, path).splitlines()
|
|
||||||
for start, _end, text in collect_function_declarations(lines):
|
|
||||||
name = text.split("(", 1)[0].split()[-1].lstrip("*")
|
|
||||||
check_kernel_doc_block(errors, path, lines, start, "function", name)
|
|
||||||
|
|
||||||
for start, _end, text in collect_public_type_declarations(lines):
|
|
||||||
name = symbol_name_from_type_declaration(text)
|
|
||||||
check_kernel_doc_block(errors, path, lines, start, "type", name)
|
|
||||||
|
|
||||||
guard = header_guard_name(lines)
|
|
||||||
for index, line in enumerate(lines):
|
|
||||||
if not is_documented_public_define(lines, index, guard):
|
|
||||||
continue
|
|
||||||
|
|
||||||
name = line.strip().split()[1].split("(", 1)[0]
|
|
||||||
check_kernel_doc_block(errors, path, lines, index, "macro", name)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def check_selftests_are_centralized(files: list[Path]) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
for path in files:
|
|
||||||
if "selftest" not in path.name:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if len(path.parts) < 2 or path.parts[:2] != ("kernel", "selftest"):
|
|
||||||
errors.append(f"{path}: kernel selftests belong under kernel/selftest/")
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def expand_make_var_token(token: str, makefile: Path) -> str:
|
|
||||||
return token.replace("$(ARCH_DIR)", "arch/x86").replace(
|
|
||||||
"$(BUILD_DIR)", "build"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def parse_makefile_sources(root: Path, makefile: Path) -> set[Path]:
|
|
||||||
text = read_text(root, makefile)
|
|
||||||
lines = text.splitlines()
|
|
||||||
sources = set()
|
|
||||||
index = 0
|
|
||||||
|
|
||||||
while index < len(lines):
|
|
||||||
line = lines[index]
|
|
||||||
match = re.match(r"^([A-Z0-9_]+)\s*:=", line)
|
|
||||||
if match == None or match.group(1) not in MAKEFILE_SOURCE_VARS:
|
|
||||||
index += 1
|
|
||||||
continue
|
|
||||||
|
|
||||||
remainder = line.split(":=", 1)[1].strip()
|
|
||||||
while True:
|
|
||||||
continued = remainder.endswith("\\")
|
|
||||||
remainder = remainder[:-1].strip() if continued else remainder
|
|
||||||
if remainder:
|
|
||||||
for token in remainder.split():
|
|
||||||
if token.endswith(".c"):
|
|
||||||
sources.add(Path(expand_make_var_token(token, makefile)))
|
|
||||||
|
|
||||||
if not continued:
|
|
||||||
break
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
if index >= len(lines):
|
|
||||||
break
|
|
||||||
remainder = lines[index].strip()
|
|
||||||
|
|
||||||
index += 1
|
|
||||||
|
|
||||||
return sources
|
|
||||||
|
|
||||||
|
|
||||||
def makefile_sources(root: Path) -> set[Path]:
|
|
||||||
sources = set()
|
|
||||||
|
|
||||||
for makefile in sorted(root.rglob("Makefile")):
|
|
||||||
if "build" in makefile.parts:
|
|
||||||
continue
|
|
||||||
sources.update(parse_makefile_sources(root, makefile.relative_to(root)))
|
|
||||||
|
|
||||||
return sources
|
|
||||||
|
|
||||||
|
|
||||||
def check_makefile_source_lists(root: Path, files: set[Path], all_mode: bool) -> list[str]:
|
|
||||||
errors = []
|
|
||||||
listed = makefile_sources(root)
|
|
||||||
existing = set(c_files(all_files(root)))
|
|
||||||
|
|
||||||
for path in sorted(listed):
|
|
||||||
if not (root / path).exists():
|
|
||||||
errors.append(f"{path}: listed in Makefile but file does not exist")
|
|
||||||
|
|
||||||
if all_mode:
|
|
||||||
for path in sorted(existing - listed):
|
|
||||||
errors.append(f"{path}: C source is not listed in a Makefile source list")
|
|
||||||
else:
|
|
||||||
for path in sorted(c_files(files)):
|
|
||||||
if path not in listed:
|
|
||||||
errors.append(
|
|
||||||
f"{path}: changed C source is not listed in a Makefile source list"
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
parser = argparse.ArgumentParser(description="Check Tianole structure rules")
|
parser = argparse.ArgumentParser(description="Check Tianole structure rules")
|
||||||
mode = parser.add_mutually_exclusive_group()
|
mode = parser.add_mutually_exclusive_group()
|
||||||
mode.add_argument("--all", action="store_true", help="check the whole tree")
|
mode.add_argument("--all", action="store_true", help="check the whole tree")
|
||||||
mode.add_argument(
|
mode.add_argument(
|
||||||
"--changed", action="store_true", help="check changed and untracked files"
|
"--changed", action="store_true", help="check changed and untracked files"
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
root = repo_root()
|
root = repo_root()
|
||||||
all_mode = not args.changed
|
all_mode = not args.changed
|
||||||
files = all_files(root) if all_mode else changed_files(root)
|
files = all_files(root) if all_mode else changed_files(root)
|
||||||
|
|
||||||
errors = []
|
errors = []
|
||||||
errors.extend(check_no_relative_parent_includes(root, source_files(files)))
|
sources = source_files(files)
|
||||||
errors.extend(check_no_bare_negative_errno(root, source_files(files)))
|
errors.extend(check_no_relative_parent_includes(root, sources))
|
||||||
errors.extend(check_scheduler_state_writes(root, source_files(files)))
|
errors.extend(check_no_bare_negative_errno(root, sources))
|
||||||
|
errors.extend(check_scheduler_state_writes(root, sources))
|
||||||
errors.extend(check_public_header_docs(root, public_headers(files)))
|
errors.extend(check_public_header_docs(root, public_headers(files)))
|
||||||
errors.extend(check_selftests_are_centralized(c_files(files)))
|
errors.extend(check_selftests_are_centralized(c_files(files)))
|
||||||
errors.extend(check_makefile_source_lists(root, files, all_mode))
|
errors.extend(check_makefile_source_lists(root, files, all_mode))
|
||||||
|
|||||||
1
scripts/tools/structure_checks/__init__.py
Normal file
1
scripts/tools/structure_checks/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
"""Structure checks for Tianole source layout."""
|
||||||
83
scripts/tools/structure_checks/common.py
Normal file
83
scripts/tools/structure_checks/common.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
SOURCE_ROOTS = ("arch", "include", "kernel", "mm")
|
||||||
|
C_SOURCE_ROOTS = ("arch", "kernel", "mm")
|
||||||
|
|
||||||
|
|
||||||
|
def repo_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parents[3]
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(root: Path, args: list[str]) -> list[str]:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", *args],
|
||||||
|
cwd=root,
|
||||||
|
check=True,
|
||||||
|
text=True,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
return [line for line in result.stdout.splitlines() if line]
|
||||||
|
|
||||||
|
|
||||||
|
def changed_files(root: Path) -> set[Path]:
|
||||||
|
files = set()
|
||||||
|
|
||||||
|
for path in run_git(root, ["diff", "--name-only", "--diff-filter=ACMR"]):
|
||||||
|
files.add(Path(path))
|
||||||
|
|
||||||
|
for path in run_git(
|
||||||
|
root, ["diff", "--cached", "--name-only", "--diff-filter=ACMR"]
|
||||||
|
):
|
||||||
|
files.add(Path(path))
|
||||||
|
|
||||||
|
for path in run_git(root, ["ls-files", "--others", "--exclude-standard"]):
|
||||||
|
files.add(Path(path))
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def all_files(root: Path) -> set[Path]:
|
||||||
|
files = set()
|
||||||
|
|
||||||
|
for base in SOURCE_ROOTS:
|
||||||
|
root_dir = root / base
|
||||||
|
if not root_dir.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
for path in root_dir.rglob("*"):
|
||||||
|
if path.is_file():
|
||||||
|
files.add(path.relative_to(root))
|
||||||
|
|
||||||
|
for path in (root / "scripts").rglob("*"):
|
||||||
|
if path.is_file():
|
||||||
|
files.add(path.relative_to(root))
|
||||||
|
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
|
def source_files(files: set[Path]) -> list[Path]:
|
||||||
|
return sorted(path for path in files if path.suffix in (".c", ".h"))
|
||||||
|
|
||||||
|
|
||||||
|
def public_headers(files: set[Path]) -> list[Path]:
|
||||||
|
return sorted(
|
||||||
|
path
|
||||||
|
for path in files
|
||||||
|
if path.suffix == ".h"
|
||||||
|
and len(path.parts) == 3
|
||||||
|
and path.parts[:2] == ("include", "tianole")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def c_files(files: set[Path]) -> list[Path]:
|
||||||
|
return sorted(
|
||||||
|
path
|
||||||
|
for path in files
|
||||||
|
if path.suffix == ".c" and path.parts and path.parts[0] in C_SOURCE_ROOTS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_text(root: Path, path: Path) -> str:
|
||||||
|
return (root / path).read_text(encoding="utf-8")
|
||||||
87
scripts/tools/structure_checks/makefiles.py
Normal file
87
scripts/tools/structure_checks/makefiles.py
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .common import all_files, c_files, read_text
|
||||||
|
|
||||||
|
|
||||||
|
MAKEFILE_SOURCE_VARS = (
|
||||||
|
"ARCH_KERNEL_SRCS",
|
||||||
|
"ARCH_MM_SRCS",
|
||||||
|
"BOOT_SRCS",
|
||||||
|
"KERNEL_SRCS",
|
||||||
|
"MM_SRCS",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_make_var_token(token: str, makefile: Path) -> str:
|
||||||
|
return token.replace("$(ARCH_DIR)", "arch/x86").replace(
|
||||||
|
"$(BUILD_DIR)", "build"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_makefile_sources(root: Path, makefile: Path) -> set[Path]:
|
||||||
|
text = read_text(root, makefile)
|
||||||
|
lines = text.splitlines()
|
||||||
|
sources = set()
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while index < len(lines):
|
||||||
|
line = lines[index]
|
||||||
|
match = re.match(r"^([A-Z0-9_]+)\s*:=", line)
|
||||||
|
if match == None or match.group(1) not in MAKEFILE_SOURCE_VARS:
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
remainder = line.split(":=", 1)[1].strip()
|
||||||
|
while True:
|
||||||
|
continued = remainder.endswith("\\")
|
||||||
|
remainder = remainder[:-1].strip() if continued else remainder
|
||||||
|
if remainder:
|
||||||
|
for token in remainder.split():
|
||||||
|
if token.endswith(".c"):
|
||||||
|
sources.add(Path(expand_make_var_token(token, makefile)))
|
||||||
|
|
||||||
|
if not continued:
|
||||||
|
break
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
if index >= len(lines):
|
||||||
|
break
|
||||||
|
remainder = lines[index].strip()
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return sources
|
||||||
|
|
||||||
|
|
||||||
|
def makefile_sources(root: Path) -> set[Path]:
|
||||||
|
sources = set()
|
||||||
|
|
||||||
|
for makefile in sorted(root.rglob("Makefile")):
|
||||||
|
if "build" in makefile.parts:
|
||||||
|
continue
|
||||||
|
sources.update(parse_makefile_sources(root, makefile.relative_to(root)))
|
||||||
|
|
||||||
|
return sources
|
||||||
|
|
||||||
|
|
||||||
|
def check_makefile_source_lists(root: Path, files: set[Path], all_mode: bool) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
listed = makefile_sources(root)
|
||||||
|
existing = set(c_files(all_files(root)))
|
||||||
|
|
||||||
|
for path in sorted(listed):
|
||||||
|
if not (root / path).exists():
|
||||||
|
errors.append(f"{path}: listed in Makefile but file does not exist")
|
||||||
|
|
||||||
|
if all_mode:
|
||||||
|
for path in sorted(existing - listed):
|
||||||
|
errors.append(f"{path}: C source is not listed in a Makefile source list")
|
||||||
|
else:
|
||||||
|
for path in sorted(c_files(files)):
|
||||||
|
if path not in listed:
|
||||||
|
errors.append(
|
||||||
|
f"{path}: changed C source is not listed in a Makefile source list"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
89
scripts/tools/structure_checks/public_header_decls.py
Normal file
89
scripts/tools/structure_checks/public_header_decls.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
def is_function_declaration_start(line: str) -> bool:
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if stripped == "" or stripped.startswith("#"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if stripped.startswith(("typedef ", "struct ", "enum ", "union ")):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "(" not in stripped or stripped.startswith("*"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
if "(*" in stripped or stripped.endswith("{"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def collect_function_declarations(lines: list[str]) -> list[tuple[int, int, str]]:
|
||||||
|
decls = []
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while index < len(lines):
|
||||||
|
if not is_function_declaration_start(lines[index]):
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = index
|
||||||
|
text = lines[index].strip()
|
||||||
|
while ";" not in lines[index]:
|
||||||
|
index += 1
|
||||||
|
if index >= len(lines):
|
||||||
|
break
|
||||||
|
text += " " + lines[index].strip()
|
||||||
|
|
||||||
|
if index < len(lines):
|
||||||
|
decls.append((start, index, text))
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return decls
|
||||||
|
|
||||||
|
|
||||||
|
def is_public_type_start(line: str) -> bool:
|
||||||
|
stripped = line.strip()
|
||||||
|
|
||||||
|
if stripped.startswith(("struct ", "enum ", "union ")):
|
||||||
|
return stripped.endswith("{") or "{" in stripped
|
||||||
|
|
||||||
|
if not stripped.startswith("typedef "):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return "(" not in stripped
|
||||||
|
|
||||||
|
|
||||||
|
def collect_public_type_declarations(lines: list[str]) -> list[tuple[int, int, str]]:
|
||||||
|
decls = []
|
||||||
|
index = 0
|
||||||
|
|
||||||
|
while index < len(lines):
|
||||||
|
if not is_public_type_start(lines[index]):
|
||||||
|
index += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
start = index
|
||||||
|
text = lines[index].strip()
|
||||||
|
while ";" not in lines[index]:
|
||||||
|
index += 1
|
||||||
|
if index >= len(lines):
|
||||||
|
break
|
||||||
|
text += " " + lines[index].strip()
|
||||||
|
|
||||||
|
if index < len(lines):
|
||||||
|
decls.append((start, index, text))
|
||||||
|
|
||||||
|
index += 1
|
||||||
|
|
||||||
|
return decls
|
||||||
|
|
||||||
|
|
||||||
|
def symbol_name_from_type_declaration(text: str) -> str:
|
||||||
|
if text.startswith("typedef "):
|
||||||
|
return text.rsplit("}", 1)[-1].strip().rstrip(";").split()[-1]
|
||||||
|
|
||||||
|
parts = text.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[1]
|
||||||
|
|
||||||
|
return "<unknown>"
|
||||||
114
scripts/tools/structure_checks/public_headers.py
Normal file
114
scripts/tools/structure_checks/public_headers.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .common import read_text
|
||||||
|
from .public_header_decls import (
|
||||||
|
collect_function_declarations,
|
||||||
|
collect_public_type_declarations,
|
||||||
|
symbol_name_from_type_declaration,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def is_documented_public_define(lines: list[str], index: int, guard: str | None) -> bool:
|
||||||
|
stripped = lines[index].strip()
|
||||||
|
|
||||||
|
if not stripped.startswith("#define "):
|
||||||
|
return False
|
||||||
|
|
||||||
|
parts = stripped.split()
|
||||||
|
if len(parts) < 2:
|
||||||
|
return False
|
||||||
|
|
||||||
|
name = parts[1].split("(", 1)[0]
|
||||||
|
if name == guard:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def header_guard_name(lines: list[str]) -> str | None:
|
||||||
|
for line in lines:
|
||||||
|
stripped = line.strip()
|
||||||
|
if stripped.startswith("#define "):
|
||||||
|
parts = stripped.split()
|
||||||
|
if len(parts) >= 2:
|
||||||
|
return parts[1]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def has_kernel_doc_before(lines: list[str], start: int) -> bool:
|
||||||
|
index = start - 1
|
||||||
|
|
||||||
|
while index >= 0 and lines[index].strip() == "":
|
||||||
|
index -= 1
|
||||||
|
|
||||||
|
if index < 0 or lines[index].strip() != "*/":
|
||||||
|
return False
|
||||||
|
|
||||||
|
while index >= 0:
|
||||||
|
if lines[index].strip().startswith("/**"):
|
||||||
|
return True
|
||||||
|
index -= 1
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_blank_line_before_doc(lines: list[str], start: int) -> bool:
|
||||||
|
index = start - 1
|
||||||
|
|
||||||
|
while index >= 0 and lines[index].strip() == "":
|
||||||
|
index -= 1
|
||||||
|
|
||||||
|
if index < 0 or lines[index].strip() != "*/":
|
||||||
|
return True
|
||||||
|
|
||||||
|
while index >= 0 and not lines[index].strip().startswith("/**"):
|
||||||
|
index -= 1
|
||||||
|
|
||||||
|
if index <= 0:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return lines[index - 1].strip() == ""
|
||||||
|
|
||||||
|
|
||||||
|
def check_kernel_doc_block(
|
||||||
|
errors: list[str], path: Path, lines: list[str], start: int, kind: str, name: str
|
||||||
|
) -> None:
|
||||||
|
line_no = start + 1
|
||||||
|
|
||||||
|
if not has_kernel_doc_before(lines, start):
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{line_no}: public {kind} '{name}' needs "
|
||||||
|
"a kernel-doc comment immediately before it"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not has_blank_line_before_doc(lines, start):
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{line_no}: public {kind} '{name}' "
|
||||||
|
"comment block must be separated by a blank line"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def check_public_header_docs(root: Path, files: list[Path]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
lines = read_text(root, path).splitlines()
|
||||||
|
for start, _end, text in collect_function_declarations(lines):
|
||||||
|
name = text.split("(", 1)[0].split()[-1].lstrip("*")
|
||||||
|
check_kernel_doc_block(errors, path, lines, start, "function", name)
|
||||||
|
|
||||||
|
for start, _end, text in collect_public_type_declarations(lines):
|
||||||
|
name = symbol_name_from_type_declaration(text)
|
||||||
|
check_kernel_doc_block(errors, path, lines, start, "type", name)
|
||||||
|
|
||||||
|
guard = header_guard_name(lines)
|
||||||
|
for index, line in enumerate(lines):
|
||||||
|
if not is_documented_public_define(lines, index, guard):
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = line.strip().split()[1].split("(", 1)[0]
|
||||||
|
check_kernel_doc_block(errors, path, lines, index, "macro", name)
|
||||||
|
|
||||||
|
return errors
|
||||||
66
scripts/tools/structure_checks/source_rules.py
Normal file
66
scripts/tools/structure_checks/source_rules.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
import re
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from .common import read_text
|
||||||
|
|
||||||
|
|
||||||
|
def check_no_relative_parent_includes(root: Path, files: list[Path]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
include_re = re.compile(r'^\s*#include\s+[<"]\.\./')
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
||||||
|
if include_re.search(line):
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{line_no}: do not use ../ includes; "
|
||||||
|
"promote shared headers or add a private include root"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def check_no_bare_negative_errno(root: Path, files: list[Path]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
return_re = re.compile(r"\breturn\s+-[0-9]+\s*;")
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
||||||
|
if return_re.search(line):
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{line_no}: return a symbolic negative errno "
|
||||||
|
"such as -EINVAL instead of a bare negative number"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def check_scheduler_state_writes(root: Path, files: list[Path]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
state_write_re = re.compile(r"->state\s*=")
|
||||||
|
allowed_path = Path("kernel/sched/sched.h")
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
if path == allowed_path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for line_no, line in enumerate(read_text(root, path).splitlines(), 1):
|
||||||
|
if state_write_re.search(line):
|
||||||
|
errors.append(
|
||||||
|
f"{path}:{line_no}: thread state writes must use "
|
||||||
|
"kernel/sched/sched.h helpers"
|
||||||
|
)
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def check_selftests_are_centralized(files: list[Path]) -> list[str]:
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for path in files:
|
||||||
|
if "selftest" not in path.name:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if len(path.parts) < 2 or path.parts[:2] != ("kernel", "selftest"):
|
||||||
|
errors.append(f"{path}: kernel selftests belong under kernel/selftest/")
|
||||||
|
|
||||||
|
return errors
|
||||||
Loading…
Reference in New Issue
Block a user