mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
Replaces the hand-written Tailwind subset in app.css with a real, purged Tailwind build: scripts/build_css.py runs the pinned, SHA-256-checked standalone Tailwind CLI (no Node), the generated tailwind.css and plugin-frame.css are committed, and CI fails when they are stale. The Pi never builds anything. The login page (#683) now links tailwind.css too, and the load-order test covers every template that links app.css. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
130 lines
4.9 KiB
Python
130 lines
4.9 KiB
Python
"""scripts/build_css.py: the pinned Tailwind CLI and what it builds.
|
|
|
|
The build itself needs the CLI download, so CI runs it in its own job
|
|
(`build_css.py --check`); these check the parts that must hold without it.
|
|
"""
|
|
|
|
import importlib.util
|
|
import re
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
PROJECT_ROOT = Path(__file__).resolve().parent.parent
|
|
spec = importlib.util.spec_from_file_location(
|
|
"build_css", PROJECT_ROOT / "scripts" / "build_css.py"
|
|
)
|
|
build_css = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(build_css)
|
|
|
|
|
|
def test_every_asset_is_pinned_to_a_sha256():
|
|
assert re.fullmatch(r"3\.\d+\.\d+", build_css.TAILWIND_VERSION)
|
|
assert build_css.TAILWIND_ASSETS
|
|
for name, digest in build_css.TAILWIND_ASSETS.items():
|
|
assert name.startswith("tailwindcss-"), name
|
|
assert re.fullmatch(r"[0-9a-f]{64}", digest), name
|
|
|
|
|
|
@pytest.mark.parametrize("system,machine,expected", [
|
|
("Linux", "x86_64", "tailwindcss-linux-x64"),
|
|
("Linux", "aarch64", "tailwindcss-linux-arm64"),
|
|
("Linux", "armv7l", "tailwindcss-linux-armv7"),
|
|
("Darwin", "arm64", "tailwindcss-macos-arm64"),
|
|
("Darwin", "x86_64", "tailwindcss-macos-x64"),
|
|
("Windows", "AMD64", "tailwindcss-windows-x64.exe"),
|
|
("Windows", "ARM64", "tailwindcss-windows-arm64.exe"),
|
|
])
|
|
def test_asset_name_maps_each_platform(monkeypatch, system, machine, expected):
|
|
monkeypatch.setattr(build_css.platform, "system", lambda: system)
|
|
monkeypatch.setattr(build_css.platform, "machine", lambda: machine)
|
|
assert build_css.asset_name() == expected
|
|
assert expected in build_css.TAILWIND_ASSETS
|
|
|
|
|
|
def test_unsupported_cpu_is_a_clear_error(monkeypatch):
|
|
monkeypatch.setattr(build_css.platform, "system", lambda: "Linux")
|
|
monkeypatch.setattr(build_css.platform, "machine", lambda: "armv6l")
|
|
with pytest.raises(SystemExit, match="armv6l"):
|
|
build_css.asset_name()
|
|
|
|
|
|
def test_every_build_input_exists_and_its_output_is_committed():
|
|
for input_css, config, output in build_css.BUILDS:
|
|
assert (PROJECT_ROOT / input_css).is_file(), input_css
|
|
assert (PROJECT_ROOT / config).is_file(), config
|
|
assert (PROJECT_ROOT / output).is_file(), output
|
|
|
|
|
|
def test_the_cli_is_fed_an_lf_copy_of_a_crlf_input(tmp_path, monkeypatch):
|
|
"""Tailwind's minifier merges rules differently when the input CSS has
|
|
CRLF line endings, so a Windows checkout built bytes CI's Linux build
|
|
didn't, and --check failed. The CLI must always see LF."""
|
|
monkeypatch.setattr(build_css, "PROJECT_ROOT", tmp_path)
|
|
(tmp_path / "in.css").write_bytes(b"@tailwind base;\r\n@tailwind utilities;\r\n")
|
|
seen = {}
|
|
|
|
def fake_run(cmd, **kwargs):
|
|
seen["input"] = Path(cmd[cmd.index("--input") + 1]).read_bytes()
|
|
Path(cmd[cmd.index("--output") + 1]).write_text(".a{b:c}", encoding="utf-8")
|
|
|
|
class Done:
|
|
returncode = 0
|
|
stdout = stderr = ""
|
|
return Done()
|
|
|
|
monkeypatch.setattr(build_css.subprocess, "run", fake_run)
|
|
work = tmp_path / "work"
|
|
work.mkdir()
|
|
out = tmp_path / "out.css"
|
|
build_css.run_build(Path("cli"), "in.css", "cfg.js", out, work)
|
|
|
|
assert seen["input"] == b"@tailwind base;\n@tailwind utilities;\n"
|
|
assert out.read_bytes() == b".a{b:c}\n"
|
|
assert (tmp_path / "in.css").read_bytes().count(b"\r\n") == 2 # source untouched
|
|
|
|
|
|
def test_a_corrupt_cached_cli_is_replaced(tmp_path, monkeypatch):
|
|
"""A cached binary that fails its hash is deleted and fetched again,
|
|
and the fresh download is hash-checked too."""
|
|
monkeypatch.setenv("LEDMATRIX_TAILWIND_CACHE", str(tmp_path))
|
|
name = build_css.asset_name()
|
|
cached = tmp_path / f"v{build_css.TAILWIND_VERSION}" / name
|
|
cached.parent.mkdir(parents=True)
|
|
cached.write_bytes(b"not the cli")
|
|
|
|
class FakeResponse:
|
|
def __init__(self, data):
|
|
self.data = data
|
|
|
|
def read(self, n=-1):
|
|
data, self.data = self.data, b""
|
|
return data
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
return False
|
|
|
|
monkeypatch.setattr(build_css.urllib.request, "urlopen",
|
|
lambda url, timeout: FakeResponse(b"tampered"))
|
|
with pytest.raises(SystemExit, match="SHA-256 mismatch"):
|
|
build_css.ensure_cli()
|
|
assert not cached.exists()
|
|
assert not any(p.name.startswith(".download-") for p in cached.parent.iterdir())
|
|
|
|
|
|
def test_the_cli_is_only_downloaded_over_https(tmp_path, monkeypatch):
|
|
"""urlopen would also follow file:// and custom schemes; the download
|
|
refuses anything but https before it opens the URL."""
|
|
monkeypatch.setenv("LEDMATRIX_TAILWIND_CACHE", str(tmp_path))
|
|
monkeypatch.setattr(build_css, "DOWNLOAD_URL", "file:///etc/{version}/{asset}")
|
|
|
|
def fail(*args, **kwargs):
|
|
raise AssertionError("urlopen must not be called for a non-https URL")
|
|
|
|
monkeypatch.setattr(build_css.urllib.request, "urlopen", fail)
|
|
with pytest.raises(SystemExit, match="non-https"):
|
|
build_css.ensure_cli()
|