Ctrl K
ComunidadGuías…

scripts/check_wheel_contents.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Wheel contents smoke test for askrex-assistant (US-015, US-016).

Builds dist/askrex_assistant-*.whl and asserts every required file is present.
Exits non-zero and names each missing file together with the install audience
that requires it.

Usage:
    python scripts/check_wheel_contents.py              # build then check
    python scripts/check_wheel_contents.py <wheel.whl>  # check a pre-built wheel
"""

from __future__ import annotations

import glob
import subprocess
import sys
import zipfile
from pathlib import Path

# ---------------------------------------------------------------------------
# Required wheel entries
#
# Each tuple is (logical_path, install_audience, description).
#
# "logical_path" is the path as it would appear directly in the wheel zip for
# package files (e.g. "rex/cli.py"), or the suffix after ".data/data/" for
# data files installed via data_files (e.g. "bridge/rex_chat_bridge.py",
# which appears in the zip as "{name}-{version}.data/data/bridge/rex_chat_bridge.py").
#
# check_wheel() resolves both forms — see its docstring for details.
# ---------------------------------------------------------------------------
REQUIRED_ENTRIES: list[tuple[str, str, str]] = [
    # -- Console script entry-point modules ----------------------------------
    # Every declared [project.scripts] entry must have its source module
    # present so `pip install` wires the console script correctly.
    ("rex/__init__.py", "developers/operators", "rex package (all console scripts)"),
    ("rex/cli.py", "developers/operators", "rex console script (rex.cli:main)"),
    ("rex/config.py", "developers/operators", "rex-config console script (rex.config:cli)"),
    ("rex/gui_app.py", "developers/operators", "rex-gui console script (rex.gui_app:main)"),
    (
        "rex/computers/agent_server.py",
        "developers/operators",
        "rex-agent console script (rex.computers.agent_server:main)",
    ),
    (
        "rex/openclaw/tool_server.py",
        "developers/operators",
        "rex-tool-server console script (rex.openclaw.tool_server:main)",
    ),
    # -- Root py_modules: backward-compat shims and entry-point module -------
    (
        "rex_speak_api.py",
        "developers/operators",
        "rex-speak-api entry-point module (rex_speak_api:main)",
    ),
    (
        "config.py",
        "developers/operators",
        "config compat shim (→ rex.config; used by test suite)",
    ),
    (
        "llm_client.py",
        "developers/operators",
        "llm_client compat shim (→ rex.llm_client; used by test suite)",
    ),
    # -- Type marker ---------------------------------------------------------
    # PEP 561: presence signals that the rex package ships inline type stubs.
    ("rex/py.typed", "developers/operators (type checkers)", "PEP 561 type marker"),
    # -- Bridge scripts (Electron desktop app) — US-016 ----------------------
    # These arrive in the wheel via data_files in setup.py.
    # In the zip they appear as "{name}-{version}.data/data/bridge/{file}".
    # check_wheel() accepts both the exact zip path and the logical suffix form.
    ("bridge/rex_calendar_bridge.py", "Electron desktop app", "calendar bridge"),
    ("bridge/rex_chat_bridge.py", "Electron desktop app", "chat bridge"),
    ("bridge/rex_chat_stream_bridge.py", "Electron desktop app", "chat stream bridge"),
    ("bridge/rex_email_bridge.py", "Electron desktop app", "email bridge"),
    ("bridge/rex_file_extract_bridge.py", "Electron desktop app", "file-extract bridge"),
    ("bridge/rex_history_bridge.py", "Electron desktop app", "history bridge"),
    ("bridge/rex_memories_bridge.py", "Electron desktop app", "memories bridge"),
    ("bridge/rex_quick_actions_bridge.py", "Electron desktop app", "quick-actions bridge"),
    ("bridge/rex_reminders_bridge.py", "Electron desktop app", "reminders bridge"),
    ("bridge/rex_setup_bridge.py", "Electron desktop app", "setup bridge"),
    ("bridge/rex_shopping_list_bridge.py", "Electron desktop app", "shopping-list bridge"),
    ("bridge/rex_sms_bridge.py", "Electron desktop app", "SMS bridge"),
    ("bridge/rex_speaker_bridge.py", "Electron desktop app", "speaker bridge"),
    ("bridge/rex_stt_bridge.py", "Electron desktop app", "STT bridge"),
    ("bridge/rex_tasks_bridge.py", "Electron desktop app", "tasks bridge"),
    ("bridge/rex_voice_bridge.py", "Electron desktop app", "voice bridge"),
    ("bridge/rex_voice_enrollment_bridge.py", "Electron desktop app", "voice enrollment bridge"),
    ("bridge/rex_voice_sample_bridge.py", "Electron desktop app", "voice sample bridge"),
    ("bridge/rex_voice_upload_bridge.py", "Electron desktop app", "voice upload bridge"),
    ("bridge/rex_voices_bridge.py", "Electron desktop app", "voices bridge"),
    ("bridge/rex_wakeword_list_bridge.py", "Electron desktop app", "wake-word list bridge"),
    ("bridge/rex_wakeword_sample_bridge.py", "Electron desktop app", "wake-word sample bridge"),
    ("bridge/rex_wakeword_train_bridge.py", "Electron desktop app", "wake-word train bridge"),
    # -- Config example — US-016 ---------------------------------------------
    # Arrives via data_files; zip path: "{name}-{version}.data/data/config/{file}".
    (
        "config/rex_config.example.json",
        "developers/operators",
        "example configuration file documented in INSTALL.md",
    ),
]


def check_wheel(wheel_path: Path) -> list[tuple[str, str, str]]:
    """Return list of (logical_path, audience, description) tuples for missing entries.

    Two matching strategies are applied for each REQUIRED_ENTRIES entry:

    1. **Exact match**: the entry path appears verbatim in the wheel zip (used for
       package files such as ``rex/cli.py``).

    2. **Data-file suffix match**: the wheel zip contains an entry of the form
       ``{name}-{version}.data/data/{suffix}`` where ``suffix`` equals the entry
       path (used for files installed via ``data_files`` in setup.py, such as
       ``bridge/rex_chat_bridge.py``).

    This means REQUIRED_ENTRIES can use logical paths like ``bridge/rex_chat_bridge.py``
    regardless of which setuptools version or wheel name is in use.
    """
    with zipfile.ZipFile(wheel_path) as zf:
        present = set(zf.namelist())

    # Build suffix → full_path mapping for data-file entries.
    # Data files in a wheel appear as "{name}-{version}.data/data/{suffix}".
    data_suffixes: dict[str, str] = {}
    for name in present:
        if ".data/data/" in name:
            suffix = name.split(".data/data/", 1)[1]
            data_suffixes[suffix] = name

    missing = []
    for entry, audience, description in REQUIRED_ENTRIES:
        if entry in present:
            continue
        if entry in data_suffixes:
            continue
        missing.append((entry, audience, description))
    return missing


def build_wheel(repo_root: Path) -> Path:
    """Run `python -m build --wheel` and return the resulting .whl path."""
    subprocess.run(
        [sys.executable, "-m", "build", "--wheel"],
        cwd=repo_root,
        check=True,
    )
    wheels = sorted(glob.glob(str(repo_root / "dist" / "askrex_assistant-*.whl")))
    if not wheels:
        raise FileNotFoundError("No wheel found in dist/ after build")
    return Path(wheels[-1])


def main(argv: list[str] | None = None) -> int:
    args = argv if argv is not None else sys.argv[1:]
    repo_root = Path(__file__).parent.parent

    if args:
        wheel_path = Path(args[0])
        if not wheel_path.exists():
            print(f"ERROR: wheel not found: {wheel_path}", file=sys.stderr)
            return 1
    else:
        print("Building wheel …")
        try:
            wheel_path = build_wheel(repo_root)
        except subprocess.CalledProcessError as exc:
            print(f"ERROR: wheel build failed: {exc}", file=sys.stderr)
            return 1
        except FileNotFoundError as exc:
            print(f"ERROR: {exc}", file=sys.stderr)
            return 1
        print(f"Built: {wheel_path}")

    missing = check_wheel(wheel_path)
    if missing:
        print(f"FAIL: {len(missing)} required file(s) missing from {wheel_path.name}:")
        for entry, audience, description in missing:
            print(f"  MISSING  {entry}")
            print(f"           audience:    {audience}")
            print(f"           description: {description}")
        return 1

    print(f"OK: all required files present in {wheel_path.name}")
    return 0


if __name__ == "__main__":
    sys.exit(main())