mirror of
https://github.com/esphome/esphome.git
synced 2025-09-01 19:02:18 +01:00
171 lines
5.1 KiB
Python
Executable File
171 lines
5.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Calculate and manage hash for clang-tidy configuration."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import hashlib
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
# Add the script directory to path to import helpers
|
|
script_dir = Path(__file__).parent
|
|
sys.path.insert(0, str(script_dir))
|
|
|
|
|
|
def read_file_lines(path: Path) -> list[str]:
|
|
"""Read lines from a file."""
|
|
with open(path) as f:
|
|
return f.readlines()
|
|
|
|
|
|
def parse_requirement_line(line: str) -> tuple[str, str] | None:
|
|
"""Parse a requirement line and return (package, original_line) or None.
|
|
|
|
Handles formats like:
|
|
- package==1.2.3
|
|
- package==1.2.3 # comment
|
|
- package>=1.2.3,<2.0.0
|
|
"""
|
|
original_line = line.strip()
|
|
|
|
# Extract the part before any comment for parsing
|
|
parse_line = line
|
|
if "#" in parse_line:
|
|
parse_line = parse_line[: parse_line.index("#")]
|
|
|
|
parse_line = parse_line.strip()
|
|
if not parse_line:
|
|
return None
|
|
|
|
# Use regex to extract package name
|
|
# This matches package names followed by version operators
|
|
match = re.match(r"^([a-zA-Z0-9_-]+)(==|>=|<=|>|<|!=|~=)(.+)$", parse_line)
|
|
if match:
|
|
return (match.group(1), original_line) # Return package name and original line
|
|
|
|
return None
|
|
|
|
|
|
def get_clang_tidy_version_from_requirements() -> str:
|
|
"""Get clang-tidy version from requirements_dev.txt"""
|
|
requirements_path = Path(__file__).parent.parent / "requirements_dev.txt"
|
|
lines = read_file_lines(requirements_path)
|
|
|
|
for line in lines:
|
|
parsed = parse_requirement_line(line)
|
|
if parsed and parsed[0] == "clang-tidy":
|
|
# Return the original line (preserves comments)
|
|
return parsed[1]
|
|
|
|
return "clang-tidy version not found"
|
|
|
|
|
|
def read_file_bytes(path: Path) -> bytes:
|
|
"""Read bytes from a file."""
|
|
with open(path, "rb") as f:
|
|
return f.read()
|
|
|
|
|
|
def calculate_clang_tidy_hash() -> str:
|
|
"""Calculate hash of clang-tidy configuration and version"""
|
|
hasher = hashlib.sha256()
|
|
|
|
# Hash .clang-tidy file
|
|
clang_tidy_path = Path(__file__).parent.parent / ".clang-tidy"
|
|
content = read_file_bytes(clang_tidy_path)
|
|
hasher.update(content)
|
|
|
|
# Hash clang-tidy version from requirements_dev.txt
|
|
version = get_clang_tidy_version_from_requirements()
|
|
hasher.update(version.encode())
|
|
|
|
# Hash the entire platformio.ini file
|
|
platformio_path = Path(__file__).parent.parent / "platformio.ini"
|
|
platformio_content = read_file_bytes(platformio_path)
|
|
hasher.update(platformio_content)
|
|
|
|
return hasher.hexdigest()
|
|
|
|
|
|
def read_stored_hash() -> str | None:
|
|
"""Read the stored hash from file"""
|
|
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
|
if hash_file.exists():
|
|
lines = read_file_lines(hash_file)
|
|
return lines[0].strip() if lines else None
|
|
return None
|
|
|
|
|
|
def write_file_content(path: Path, content: str) -> None:
|
|
"""Write content to a file."""
|
|
with open(path, "w") as f:
|
|
f.write(content)
|
|
|
|
|
|
def write_hash(hash_value: str) -> None:
|
|
"""Write hash to file"""
|
|
hash_file = Path(__file__).parent.parent / ".clang-tidy.hash"
|
|
# Strip any trailing newlines to ensure consistent formatting
|
|
write_file_content(hash_file, hash_value.strip() + "\n")
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(description="Manage clang-tidy configuration hash")
|
|
parser.add_argument(
|
|
"--check",
|
|
action="store_true",
|
|
help="Check if full scan needed (exit 0 if needed)",
|
|
)
|
|
parser.add_argument("--update", action="store_true", help="Update the hash file")
|
|
parser.add_argument(
|
|
"--update-if-changed",
|
|
action="store_true",
|
|
help="Update hash only if configuration changed (for pre-commit)",
|
|
)
|
|
parser.add_argument(
|
|
"--verify", action="store_true", help="Verify hash matches (for CI)"
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
current_hash = calculate_clang_tidy_hash()
|
|
stored_hash = read_stored_hash()
|
|
|
|
if args.check:
|
|
# Exit 0 if full scan needed (hash changed or no hash file)
|
|
sys.exit(0 if current_hash != stored_hash else 1)
|
|
|
|
elif args.update:
|
|
write_hash(current_hash)
|
|
print(f"Hash updated: {current_hash}")
|
|
|
|
elif args.update_if_changed:
|
|
if current_hash != stored_hash:
|
|
write_hash(current_hash)
|
|
print(f"Clang-tidy hash updated: {current_hash}")
|
|
# Exit 0 so pre-commit can stage the file
|
|
sys.exit(0)
|
|
else:
|
|
print("Clang-tidy hash unchanged")
|
|
sys.exit(0)
|
|
|
|
elif args.verify:
|
|
if current_hash != stored_hash:
|
|
print("ERROR: Clang-tidy configuration has changed but hash not updated!")
|
|
print(f"Expected: {current_hash}")
|
|
print(f"Found: {stored_hash}")
|
|
print("\nPlease run: script/clang_tidy_hash.py --update")
|
|
sys.exit(1)
|
|
print("Hash verification passed")
|
|
|
|
else:
|
|
print(f"Current hash: {current_hash}")
|
|
print(f"Stored hash: {stored_hash}")
|
|
print(f"Match: {current_hash == stored_hash}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|