d76461d774
The two board profiles (5,000 lines each, 87 % the same text) become packages/core.yaml and one file per board under packages/boards/: the hardware, a table of 84 sizes and font sizes, 18 hooks with the lines of C++ only that board adds inside a shared lambda, and the board's own sections. packages/cyd.yaml and packages/guition.yaml keep their names and places (a screen installed from ESP Screens builds as before); home-like-2432s028.yaml and guition-4848s040.yaml become the same entries for a checkout. The firmware stays 0.2.70: the C++ ESPHome generates for each board, through the checkout profile and through the GitHub package route, was compared line by line with the old profiles' (docs/TEST_RESULTS_0284.md). tools/check_packages.py replaces tools/generate_packages.py; tools/profiles.py gives the tests and tools the three files as one; docs/PROFILES.md says how the files fit together and how a board is added. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
86 lines
4.6 KiB
Python
86 lines
4.6 KiB
Python
"""Check that the packages a screen builds from fit together (app 0.2.84+; tools/check.sh runs this).
|
|
|
|
A screen is packages/core.yaml plus one board file under packages/boards/, included by an entry file: packages/<board>.yaml
|
|
for a screen that builds over GitHub, <profile>.yaml in the repository root for a build from a checkout. This checks what
|
|
ESPHome would only tell one build at a time:
|
|
|
|
- every entry names files that exist, and the published entry and the checkout entry of a board include the same two;
|
|
- nothing under packages/ carries a secret, a local component path or a fixed Home Assistant subscription: a screen
|
|
gets its values from ESP Screen Manager while it runs, and its keys from its own YAML;
|
|
- every ${NAME} the shared core uses is defined, and every board defines the same names: a new board file that
|
|
forgets a size or a hook fails here, not in the first build of a user.
|
|
"""
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
import profiles # noqa: E402
|
|
|
|
ROOT = profiles.ROOT
|
|
PLACEHOLDER = re.compile(r'\$\{([A-Z_][A-Z0-9_]*)\}')
|
|
# Defined by the entry files (the fonts' place) or by ESP Screen Manager's screen YAML.
|
|
FROM_ENTRY = {'FONT_DIR'}
|
|
|
|
|
|
def included(path):
|
|
text = path.read_text()
|
|
block = re.search(r'^packages:\n(.*?)(?=^[a-z_]+:|\Z)', text, re.M | re.S)
|
|
return [str((path.parent / name).resolve().relative_to(ROOT)) for name in re.findall(r'!include (\S+)', block[1])] if block else []
|
|
|
|
|
|
def fail(message):
|
|
raise SystemExit(f'check_packages: {message}')
|
|
|
|
|
|
def main():
|
|
for board in profiles.BOARDS:
|
|
entries = [name for name, b in profiles.ENTRIES.items() if b == board]
|
|
wanted = {'packages/core.yaml', str(profiles.BOARDS[board].relative_to(ROOT))}
|
|
for name in entries:
|
|
files = included(ROOT / name)
|
|
if set(files) != wanted:
|
|
fail(f'{name} includes {files}, expected {sorted(wanted)}')
|
|
for file in files:
|
|
if not (ROOT / file).exists():
|
|
fail(f'{name} includes a file that does not exist: {file}')
|
|
for path in sorted((ROOT / 'packages').rglob('*.yaml')):
|
|
text = '\n'.join(line for line in path.read_text().split('\n') if not line.lstrip().startswith('#'))
|
|
for needle, why in (('!secret', 'a secret'), ('type: local', 'a local component path')):
|
|
if needle in text:
|
|
fail(f'{path.relative_to(ROOT)} carries {why} ({needle})')
|
|
# A screen gets its values from ESP Screen Manager while it runs: no fixed subscription (time: is the exception).
|
|
for section in ('sensor', 'binary_sensor', 'text_sensor'):
|
|
match = re.search(r'^' + section + r':\n.*?(?=^[a-zA-Z_]+:|\Z)', text, re.M | re.S)
|
|
if match and re.search(r'^ - platform: homeassistant$', match[0], re.M):
|
|
fail(f'{path.relative_to(ROOT)}: a {section} subscribes to Home Assistant; runtime tiles bring their own values')
|
|
# The names the core uses, comment lines left out (its header says "${NAME}" in words).
|
|
core = '\n'.join(line for line in profiles.CORE.read_text().split('\n') if not line.lstrip().startswith('#'))
|
|
core_names = set(profiles.substitutions_of(profiles.CORE))
|
|
used = set(PLACEHOLDER.findall(core))
|
|
board_names = {}
|
|
for board, path in profiles.BOARDS.items():
|
|
values = profiles.substitutions_of(path)
|
|
board_names[board] = set(values)
|
|
# A hook's code may name sizes of its own.
|
|
used_here = used | {name for value in values.values() for name in PLACEHOLDER.findall(value)}
|
|
missing = used_here - core_names - set(values) - FROM_ENTRY
|
|
if missing:
|
|
fail(f'{path.relative_to(ROOT)} does not define {sorted(missing)}, which the shared core uses')
|
|
boards = list(board_names)
|
|
for a in boards:
|
|
for b in boards:
|
|
only = board_names[a] - board_names[b]
|
|
only -= {name for name in only if name.startswith(('TOUCH_AFFINE_', 'TOUCH_CAL_', 'EDGE_SWIPE_', 'ALERT_CARD_H_IMAGE', 'ALERT_IMAGE_', 'ALERT_SUBTITLE_H_IMAGE', 'CAMERA_'))}
|
|
if only:
|
|
fail(f'{profiles.BOARDS[a].name} defines {sorted(only)}, which {profiles.BOARDS[b].name} lacks: a board-only name belongs in that board\'s own sections, a shared one in both')
|
|
unused = core_names - used
|
|
for board, path in profiles.BOARDS.items():
|
|
board_text = path.read_text()
|
|
unused -= set(PLACEHOLDER.findall(board_text))
|
|
print(f'packages: {len(used)} names in the shared core, defined by every board' + (f'; unused in the core: {sorted(unused)}' if unused else ''))
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|