Files
tw93--kami/scripts/render.py
T
Tw93 e4a297390e feat: strengthen artifact and release verification
Make artifact briefs, capability checks, screenshots, and required assets fail closed. Preserve the last good render, move heavy tests onto the covered CI path, and require exact tag, SHA, and package identity before release upload.
2026-08-01 23:20:59 +08:00

159 lines
5.0 KiB
Python

"""Artifact production primitives shared by build, verify, and the MCP server.
One home for the render pipeline (read HTML, highlight code blocks, WeasyPrint
to PDF, stamp Kami metadata, count pages) and the PPTX fallback build. Before
this module existed the pipeline lived in build.py and was duplicated by
verify.py (through injected callbacks, to dodge a circular import) and the MCP
server (through a late import). Keeping it here, below build.py in the import
graph, dissolves both workarounds.
"""
from __future__ import annotations
import functools
import os
import subprocess
import sys
import tempfile
from pathlib import Path
from highlight import highlight_code_blocks
from optional_deps import (
MissingDepError,
require_pypdf_reader,
require_pypdf_writer,
require_weasyprint_html,
)
from shared import EXAMPLES, TEMPLATES, pptx_targets
@functools.lru_cache(maxsize=1)
def infer_author() -> str:
"""Infer author name from git config or environment.
Priority:
1. git config user.name
2. KAMI_AUTHOR env var
3. fallback to "Kami"
Cached so a full build doesn't shell out for every PDF target.
"""
try:
result = subprocess.run(
["git", "config", "user.name"],
capture_output=True,
text=True,
check=False,
)
if result.returncode == 0 and result.stdout.strip():
return result.stdout.strip()
except FileNotFoundError:
pass
if env_author := os.environ.get("KAMI_AUTHOR"):
return env_author
return "Kami"
def set_pdf_metadata(pdf_path: Path, author: str | None = None) -> None:
"""Set PDF metadata using pypdf, only if placeholders are still present."""
try:
PdfReader = require_pypdf_reader()
PdfWriter = require_pypdf_writer()
except MissingDepError:
return
if not pdf_path.exists():
return
reader = PdfReader(str(pdf_path))
existing = reader.metadata or {}
needs_update = False
metadata = dict(existing)
if author and existing.get("/Author"):
author_value = str(existing["/Author"])
if "{{" in author_value and "}}" in author_value:
metadata["/Author"] = author
needs_update = True
if metadata.get("/Producer") != "Kami":
metadata["/Producer"] = "Kami"
needs_update = True
if metadata.get("/Creator") != "Kami":
metadata["/Creator"] = "Kami"
needs_update = True
if not needs_update:
return
writer = PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.add_metadata(metadata)
with open(pdf_path, "wb") as f:
writer.write(f)
def render_pdf(src: Path, out: Path) -> int:
"""Render an HTML file to PDF and return its page count.
The full pipeline every caller must agree on: build-time code highlighting,
WeasyPrint with base_url at the source directory, Kami PDF metadata, page
count via pypdf. Raises MissingDepError when weasyprint/pypdf are absent;
callers decide how to report it.
"""
HTML = require_weasyprint_html()
PdfReader = require_pypdf_reader()
out.parent.mkdir(parents=True, exist_ok=True)
html_text = highlight_code_blocks(src.read_text(encoding="utf-8"))
# Build and validate beside the destination, then atomically replace it.
# Metadata stamping and the final page read can still fail after WeasyPrint
# succeeds; writing straight to `out` would destroy the last good artifact
# before the caller learns that the render failed.
with tempfile.TemporaryDirectory(
dir=out.parent,
prefix=f".{out.name}-",
) as staging_dir:
candidate = Path(staging_dir) / out.name
HTML(string=html_text, base_url=str(src.parent)).write_pdf(str(candidate))
set_pdf_metadata(candidate, author=infer_author())
page_count = len(PdfReader(str(candidate)).pages)
os.replace(candidate, out)
return page_count
def build_slides(name: str = "slides") -> bool:
"""Run a python-pptx slide script from the shared registry; True on success."""
source = pptx_targets().get(name)
if source is None:
print(f"ERROR: {name}: unknown slides target")
return False
src = TEMPLATES / source
if not src.exists():
print(f"ERROR: {name}: source not found ({src})")
return False
EXAMPLES.mkdir(parents=True, exist_ok=True)
out = EXAMPLES / f"{name}.pptx"
# Pass --out so the slides script writes directly to the target path. Older
# slides.py defaults to 'output.pptx' in cwd; new copies accept --out.
result = subprocess.run(
[sys.executable, str(src), "--out", str(out)],
cwd=str(src.parent),
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"ERROR: {name}: {result.stderr.strip() or 'script failed'}")
return False
if out.exists():
print(f"OK: {name}: generated {out.name}")
return True
print(f"ERROR: {name}: {out.name} not produced")
return False