#!/bin/bash # Vendoring script for hermes-chatto-plugin # # Downloads chattolib and all its dependencies with uv and vendors them into # vendor/. Two of them (protobuf, websockets) ship compiled extension modules, # so the tree is split by platform: # # vendor/common/ pure-Python packages, shared # vendor/platform// compiled extensions, one dir per platform # # vendor_path.py picks the right directory at import time. # # Usage: # ./vendor_chattolib.sh # all default platforms # PLATFORMS="linux-x86_64" ./vendor_chattolib.sh # CHATTOLIB_SPEC="chattolib[realtime]==0.5.0a5.post1" ./vendor_chattolib.sh # VENDOR_DIR="/tmp/vendor-alt" ./vendor_chattolib.sh set -euo pipefail PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" # Overridable from the environment; unset or empty falls back to the plugin's # own vendor/ so neither `set -u` nor a stray empty value can break startup. VENDOR_DIR="${VENDOR_DIR:-$PLUGIN_DIR/vendor}" BUILD_DIR="$(mktemp -d)" trap 'rm -rf "$BUILD_DIR"' EXIT # The [realtime] extra pulls in websockets, which chattolib's event stream # imports at connect time. Without it the plugin registers fine and only # starts warn-looping once the gateway connects — vendor it. CHATTOLIB_SPEC="${CHATTOLIB_SPEC:-chattolib[realtime]>=0.5.0b4,<0.6}" # Wheels are resolved for this Python version. 3.11 is the project's minimum # (pyproject: requires-python) AND the version for which the binary packages # still provide cp310-abi3 wheels — those are forward compatible, so one build # covers 3.11, 3.12, 3.13 and 3.14. Do not raise this without checking that # the resulting .so files are still abi3. PYTHON_VERSION="${PYTHON_VERSION:-3.11}" # Platform tag (as used in vendor/platform/ and by vendor_path.py) mapped to # the uv --python-platform target. Add entries here to support more platforms; # vendor_path.py derives the same tags from platform.system()/machine(). platform_target() { case "$1" in linux-x86_64) echo "x86_64-unknown-linux-gnu" ;; linux-aarch64) echo "aarch64-unknown-linux-gnu" ;; linux-x86_64-musl) echo "x86_64-unknown-linux-musl" ;; linux-aarch64-musl) echo "aarch64-unknown-linux-musl" ;; macos-arm64) echo "aarch64-apple-darwin" ;; macos-x86_64) echo "x86_64-apple-darwin" ;; windows-amd64) echo "x86_64-pc-windows-msvc" ;; *) return 1 ;; esac } # Default set: Linux (deployment, both architectures) and Apple Silicon (dev). DEFAULT_PLATFORMS="linux-x86_64 linux-aarch64 macos-arm64" read -r -a PLATFORM_LIST <<< "${PLATFORMS:-$DEFAULT_PLATFORMS}" if ! command -v uv > /dev/null 2>&1; then echo "❌ uv nicht gefunden — siehe https://docs.astral.sh/uv/" >&2 exit 1 fi echo "📦 Vendoring '$CHATTOLIB_SPEC' für: ${PLATFORM_LIST[*]}" echo " (Python $PYTHON_VERSION, nur Binär-Wheels — kein Bauen aus Sourcen)" for tag in "${PLATFORM_LIST[@]}"; do if ! target="$(platform_target "$tag")"; then echo "❌ Unbekannte Plattform '$tag'. Bekannt: linux-x86_64, linux-aarch64," >&2 echo " linux-x86_64-musl, linux-aarch64-musl, macos-arm64, macos-x86_64, windows-amd64" >&2 exit 1 fi echo " → $tag ($target)" # --only-binary :all: for everything: a source distribution could not be # built for a foreign platform, and building it for the host would # silently produce artifacts for the wrong architecture. Every wheel the # spec resolves to is downloaded into vendor/ — which is not the same as # what is imported: setup_vendor_path appends common/, so wherever Hermes # already ships a package the host's copy wins (see VENDORING.md). uv pip install \ --target "$BUILD_DIR/$tag" \ --python-platform "$target" \ --python-version "$PYTHON_VERSION" \ --only-binary :all: \ --quiet \ "$CHATTOLIB_SPEC" done echo "🧹 Bereinige alten vendor-Ordner..." # Guard before the destructive step: this script must never run rm -rf with # an empty or unset target, whatever future edits do to the assignment above. : "${VENDOR_DIR:?VENDOR_DIR ist nicht gesetzt — Abbruch vor rm -rf}" rm -rf "$VENDOR_DIR" mkdir -p "$VENDOR_DIR" echo "✂️ Trenne plattformunabhängige von binären Paketen..." python3 "$PLUGIN_DIR/vendor_split.py" "$BUILD_DIR" "$VENDOR_DIR" "${PLATFORM_LIST[@]}" echo "📄 Erstelle __init__.py Marker..." touch "$VENDOR_DIR/__init__.py" find "$VENDOR_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2> /dev/null || true # Die Konsolenskripte der Pakete (httpx, idna, websockets) bekommen von uv eine # Shebang mit dem absoluten Pfad des venv, aus dem gebaut wurde. Die landet so # im ausgelieferten Baum und macht ihn unreproduzierbar: derselbe Refresh auf # einem anderen Rechner ändert fünf Dateien, ohne dass sich am Code etwas # geändert hätte. Sie werden nie ausgeführt (der Baum kommt über sys.path in # sys.path-Einträge, nicht als Kommandos), also auf den portablen Shebang # normalisieren. echo "🔧 Normalisiere Shebangs der Konsolenskripte..." find "$VENDOR_DIR" -type f -path "*/bin/*" -exec sed -i '1s|^#!.*|#!/usr/bin/env python3|' {} + 2> /dev/null || true # …und danach die RECORDs wieder mit dem Baum in Einklang bringen. Wer den Hash # nicht anpasst, liefert fünf Dateien aus, deren Prüfsumme nicht mehr stimmt — # das fällt erst beim Installer auf, nicht hier. Rekursiv über alle RECORDs, weil # das Aufräumen von __pycache__ oben ebenfalls Einträge entfernt hat. echo "🔧 Aktualisiere RECORD-Prüfsummen..." python3 - "$VENDOR_DIR" <<'PYTHON_RECORD' import base64 import csv import hashlib import sys from pathlib import Path vendor = Path(sys.argv[1]) fixed = 0 for record in vendor.rglob("RECORD"): base = record.parent.parent rows = list(csv.reader(record.read_text(encoding="utf-8").splitlines())) changed = False for row in rows: if len(row) < 3 or not row[0] or "=" not in row[1]: continue target = base / row[0] if not target.is_file(): continue digest = base64.urlsafe_b64encode( hashlib.sha256(target.read_bytes()).digest() ).rstrip(b"=").decode() size = str(target.stat().st_size) if row[1] != f"sha256={digest}" or row[2] != size: row[1], row[2] = f"sha256={digest}", size changed = True if changed: out = [] for row in rows: out.append(",".join(row)) record.write_text("\n".join(out) + "\n", encoding="utf-8") fixed += 1 print(f" {fixed} RECORD(s) angepasst") PYTHON_RECORD echo "🔍 Verifiziere Import für die aktuelle Plattform..." if python3 -c " import sys sys.path.insert(0, '$PLUGIN_DIR') from vendor_path import setup_vendor_path, platform_tag try: setup_vendor_path() except ImportError as exc: print(f' ⓘ {exc}') sys.exit(0) import chattolib.client import chattolib.realtime print(f' ✓ chattolib inkl. realtime importiert ({platform_tag()})') "; then : else echo "❌ Import-Verifikation fehlgeschlagen" >&2 exit 1 fi echo "✓ Fertig: $(du -sh "$VENDOR_DIR" | cut -f1) in $VENDOR_DIR" echo " 'git add vendor/' nicht vergessen."