vendor_chattolib.sh 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  1. #!/bin/bash
  2. # Vendoring script for hermes-chatto-plugin
  3. #
  4. # Downloads chattolib and all its dependencies with uv and vendors them into
  5. # vendor/. Three of them (pyqwest, protobuf, protobuf-py-ext) ship compiled
  6. # extension modules, so the tree is split by platform:
  7. #
  8. # vendor/common/ pure-Python packages, shared
  9. # vendor/platform/<tag>/ compiled extensions, one dir per platform
  10. #
  11. # vendor_path.py picks the right directory at import time.
  12. #
  13. # Usage:
  14. # ./vendor_chattolib.sh # all default platforms
  15. # PLATFORMS="linux-x86_64" ./vendor_chattolib.sh
  16. # CHATTOLIB_SPEC="chattolib[realtime]==0.5.0a5.post1" ./vendor_chattolib.sh
  17. # VENDOR_DIR="/tmp/vendor-alt" ./vendor_chattolib.sh
  18. set -euo pipefail
  19. PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
  20. # Overridable from the environment; unset or empty falls back to the plugin's
  21. # own vendor/ so neither `set -u` nor a stray empty value can break startup.
  22. VENDOR_DIR="${VENDOR_DIR:-$PLUGIN_DIR/vendor}"
  23. BUILD_DIR="$(mktemp -d)"
  24. trap 'rm -rf "$BUILD_DIR"' EXIT
  25. # The [realtime] extra pulls in websockets, which chattolib's event stream
  26. # imports at connect time. Without it the plugin registers fine and only
  27. # starts warn-looping once the gateway connects — vendor it.
  28. CHATTOLIB_SPEC="${CHATTOLIB_SPEC:-chattolib[realtime]>=0.5.0b4,<0.6}"
  29. # Wheels are resolved for this Python version. 3.11 is the project's minimum
  30. # (pyproject: requires-python) AND the version for which the binary packages
  31. # still provide cp310-abi3 wheels — those are forward compatible, so one build
  32. # covers 3.11, 3.12, 3.13 and 3.14. Do not raise this without checking that
  33. # the resulting .so files are still abi3.
  34. PYTHON_VERSION="${PYTHON_VERSION:-3.11}"
  35. # Platform tag (as used in vendor/platform/ and by vendor_path.py) mapped to
  36. # the uv --python-platform target. Add entries here to support more platforms;
  37. # vendor_path.py derives the same tags from platform.system()/machine().
  38. platform_target() {
  39. case "$1" in
  40. linux-x86_64) echo "x86_64-unknown-linux-gnu" ;;
  41. linux-aarch64) echo "aarch64-unknown-linux-gnu" ;;
  42. linux-x86_64-musl) echo "x86_64-unknown-linux-musl" ;;
  43. linux-aarch64-musl) echo "aarch64-unknown-linux-musl" ;;
  44. macos-arm64) echo "aarch64-apple-darwin" ;;
  45. macos-x86_64) echo "x86_64-apple-darwin" ;;
  46. windows-amd64) echo "x86_64-pc-windows-msvc" ;;
  47. *) return 1 ;;
  48. esac
  49. }
  50. # Default set: Linux (deployment, both architectures) and Apple Silicon (dev).
  51. DEFAULT_PLATFORMS="linux-x86_64 linux-aarch64 macos-arm64"
  52. read -r -a PLATFORM_LIST <<< "${PLATFORMS:-$DEFAULT_PLATFORMS}"
  53. if ! command -v uv > /dev/null 2>&1; then
  54. echo "❌ uv nicht gefunden — siehe https://docs.astral.sh/uv/" >&2
  55. exit 1
  56. fi
  57. echo "📦 Vendoring '$CHATTOLIB_SPEC' für: ${PLATFORM_LIST[*]}"
  58. echo " (Python $PYTHON_VERSION, nur Binär-Wheels — kein Bauen aus Sourcen)"
  59. for tag in "${PLATFORM_LIST[@]}"; do
  60. if ! target="$(platform_target "$tag")"; then
  61. echo "❌ Unbekannte Plattform '$tag'. Bekannt: linux-x86_64, linux-aarch64," >&2
  62. echo " linux-x86_64-musl, linux-aarch64-musl, macos-arm64, macos-x86_64, windows-amd64" >&2
  63. exit 1
  64. fi
  65. echo " → $tag ($target)"
  66. # --only-binary :all: is required: a source distribution could not be built
  67. # for a foreign platform, and building it for the host would silently
  68. # produce artifacts for the wrong architecture.
  69. #
  70. # chattolib is the one exception: it is pure Python with no compiled
  71. # extension, so building it from the sdist on any host yields identical,
  72. # platform-independent files. The exception was load-bearing while upstream
  73. # published sdist-only releases (b5…b14); it now also pins the *source* we
  74. # vendor, so the tree does not change shape the day a release is
  75. # re-uploaded as a wheel.
  76. uv pip install \
  77. --target "$BUILD_DIR/$tag" \
  78. --python-platform "$target" \
  79. --python-version "$PYTHON_VERSION" \
  80. --only-binary :all: \
  81. --no-binary chattolib \
  82. --quiet \
  83. "$CHATTOLIB_SPEC"
  84. done
  85. echo "🧹 Bereinige alten vendor-Ordner..."
  86. # Guard before the destructive step: this script must never run rm -rf with
  87. # an empty or unset target, whatever future edits do to the assignment above.
  88. : "${VENDOR_DIR:?VENDOR_DIR ist nicht gesetzt — Abbruch vor rm -rf}"
  89. rm -rf "$VENDOR_DIR"
  90. mkdir -p "$VENDOR_DIR"
  91. echo "✂️ Trenne plattformunabhängige von binären Paketen..."
  92. python3 "$PLUGIN_DIR/vendor_split.py" "$BUILD_DIR" "$VENDOR_DIR" "${PLATFORM_LIST[@]}"
  93. echo "📄 Erstelle __init__.py Marker..."
  94. touch "$VENDOR_DIR/__init__.py"
  95. find "$VENDOR_DIR" -type d -name "__pycache__" -exec rm -rf {} + 2> /dev/null || true
  96. # Die Konsolenskripte der Pakete (httpx, idna, websockets) bekommen von uv eine
  97. # Shebang mit dem absoluten Pfad des venv, aus dem gebaut wurde. Die landet so
  98. # im ausgelieferten Baum und macht ihn unreproduzierbar: derselbe Refresh auf
  99. # einem anderen Rechner ändert fünf Dateien, ohne dass sich am Code etwas
  100. # geändert hätte. Sie werden nie ausgeführt (der Baum kommt über sys.path in
  101. # sys.path-Einträge, nicht als Kommandos), also auf den portablen Shebang
  102. # normalisieren.
  103. echo "🔧 Normalisiere Shebangs der Konsolenskripte..."
  104. find "$VENDOR_DIR" -type f -path "*/bin/*" -exec sed -i '1s|^#!.*|#!/usr/bin/env python3|' {} + 2> /dev/null || true
  105. # …und danach die RECORDs wieder mit dem Baum in Einklang bringen. Wer den Hash
  106. # nicht anpasst, liefert fünf Dateien aus, deren Prüfsumme nicht mehr stimmt —
  107. # das fällt erst beim Installer auf, nicht hier. Rekursiv über alle RECORDs, weil
  108. # das Aufräumen von __pycache__ oben ebenfalls Einträge entfernt hat.
  109. echo "🔧 Aktualisiere RECORD-Prüfsummen..."
  110. python3 - "$VENDOR_DIR" <<'PYTHON_RECORD'
  111. import base64
  112. import csv
  113. import hashlib
  114. import sys
  115. from pathlib import Path
  116. vendor = Path(sys.argv[1])
  117. fixed = 0
  118. for record in vendor.rglob("RECORD"):
  119. base = record.parent.parent
  120. rows = list(csv.reader(record.read_text(encoding="utf-8").splitlines()))
  121. changed = False
  122. for row in rows:
  123. if len(row) < 3 or not row[0] or "=" not in row[1]:
  124. continue
  125. target = base / row[0]
  126. if not target.is_file():
  127. continue
  128. digest = base64.urlsafe_b64encode(
  129. hashlib.sha256(target.read_bytes()).digest()
  130. ).rstrip(b"=").decode()
  131. size = str(target.stat().st_size)
  132. if row[1] != f"sha256={digest}" or row[2] != size:
  133. row[1], row[2] = f"sha256={digest}", size
  134. changed = True
  135. if changed:
  136. out = []
  137. for row in rows:
  138. out.append(",".join(row))
  139. record.write_text("\n".join(out) + "\n", encoding="utf-8")
  140. fixed += 1
  141. print(f" {fixed} RECORD(s) angepasst")
  142. PYTHON_RECORD
  143. echo "🔍 Verifiziere Import für die aktuelle Plattform..."
  144. if python3 -c "
  145. import sys
  146. sys.path.insert(0, '$PLUGIN_DIR')
  147. from vendor_path import setup_vendor_path, platform_tag
  148. try:
  149. setup_vendor_path()
  150. except ImportError as exc:
  151. print(f' ⓘ {exc}')
  152. sys.exit(0)
  153. import chattolib.client
  154. import chattolib.realtime
  155. print(f' ✓ chattolib inkl. realtime importiert ({platform_tag()})')
  156. "; then
  157. :
  158. else
  159. echo "❌ Import-Verifikation fehlgeschlagen" >&2
  160. exit 1
  161. fi
  162. echo "✓ Fertig: $(du -sh "$VENDOR_DIR" | cut -f1) in $VENDOR_DIR"
  163. echo " 'git add vendor/' nicht vergessen."