#!/usr/bin/env bash # Bezugskette der drei Sammlungen: laden (oder Cache), SHA-256 pruefen, # nach build/vendor/ legen. Idempotent: baut jedes Mal build/vendor/ neu aus # dem Cache; geladen wird nur, was im Cache fehlt. Bricht bei falschem Hash ab. # # Quelle aller Versionen und Hashes: vendor/manifest.json. # Cache: $SWT_CACHE (Default ~/.cache/sw-labs-tools), ausserhalb des Repos, # damit 400 MB Artefakte nie in Git landen. # # Regeln (AGENTS.md): Dateien der Sammlungen werden nicht editiert. Erlaubt # sind nur (a) Weglassen ganzer Dateien (ueber 25 MiB oder Werkzeug # weggelassen), (b) Laufzeit-Konfigurationsdateien, die die Sammlung selbst # zum Ersetzen vorsieht (IT-Tools tools-settings.json, tools-filter.json), # (c) Kopien unter neuem Namen (CyberChef index.html). set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" MANIFEST="$ROOT/vendor/manifest.json" CACHE="${SWT_CACHE:-$HOME/.cache/sw-labs-tools}" OUT="$ROOT/build/vendor" TMP="$ROOT/build/vendor.tmp" log() { echo "vendor: $*" >&2; } die() { echo "vendor: FEHLER: $*" >&2; exit 1; } command -v python3 >/dev/null || die "python3 fehlt" command -v unzip >/dev/null || die "unzip fehlt" command -v docker >/dev/null || die "docker fehlt (BentoPDF-Build im Container)" mkdir -p "$CACHE" # Manifest -> Zeilen "dateibezugsha256" (Sammlungen und WASM-Pakete). eintraege() { python3 -I -c ' import json, sys m = json.load(open(sys.argv[1])) seen = set() for e in m["sammlungen"] + m["wasm_pakete"]: print(e["datei"], e["bezug"], e["sha256"], sep="\t") seen.add(e["datei"]) # Quell-Tarballs der Tags (Werkzeugindex); bei BentoPDF identisch mit "datei". for e in m["sammlungen"]: if e["quelle_datei"] not in seen: print(e["quelle_datei"], e["quelle_archiv"], e["quelle_sha256"], sep="\t") ' "$MANIFEST" } # Holt eine Datei in den Cache und prueft den Hash. Ein frischer Download # landet erst unter .part und wird nur bei passendem Hash umbenannt. holen() { local datei="$1" url="$2" soll="$3" ist if [ ! -f "$CACHE/$datei" ]; then log "lade $datei" curl -fsSL --retry 3 "$url" -o "$CACHE/$datei.part" mv "$CACHE/$datei.part" "$CACHE/$datei" fi ist="$(sha256sum "$CACHE/$datei" | cut -d' ' -f1)" [ "$ist" = "$soll" ] || die "Hash falsch fuer $datei: ist $ist, soll $soll" } while IFS=$'\t' read -r datei url sha; do holen "$datei" "$url" "$sha" done < <(eintraege) log "alle $(eintraege | wc -l) Artefakte im Cache, Hashes stimmen" rm -rf "$TMP" mkdir -p "$TMP" # --- IT-Tools --------------------------------------------------------------- mkdir -p "$TMP/x-it-tools" unzip -q "$CACHE/it-tools-2026.10.25.zip" -d "$TMP/x-it-tools" mv "$TMP/x-it-tools/dist" "$TMP/it-tools" rm -rf "$TMP/x-it-tools" # ffmpeg-core (32.232.419 B) liegt ueber der 25-MiB-Grenze und dient nur # gif-to-mp4; das Werkzeug fliegt per tools-filter.json raus. rm -f "$TMP"/it-tools/assets/ffmpeg-core-*.wasm # Laufzeit-Konfiguration, die IT-Tools zum Ersetzen vorsieht (README, # "Offline / air-gapped deployments" und "default UI language"). # offline: blendet Werkzeuge aus, die nur mit Internet gehen (my-ip, DNS, # Waehrung, KI-Modelle ...); de.yml liegt im Fork, also default_locale de. printf '{\n "offline": true,\n "default_locale": "de"\n}\n' > "$TMP/it-tools/tools-settings.json" # Regex auf den Werkzeugpfad (src/tools/index.ts, excludeToolsFilterRegex). printf '{\n "excludeToolsFilterRegex": "^/gif-to-mp4$"\n}\n' > "$TMP/it-tools/tools-filter.json" log "it-tools fertig" # --- CyberChef -------------------------------------------------------------- mkdir -p "$TMP/cyberchef" unzip -q "$CACHE/CyberChef_8cd426dd4f40f1423912d5fad91b578a86a65112.zip" -d "$TMP/cyberchef" # Das Release hat keine index.html; Kopie, damit /cyberchef/ direkt geht. cp "$TMP/cyberchef/CyberChef_v11.5.0.html" "$TMP/cyberchef/index.html" log "cyberchef fertig" # --- BentoPDF --------------------------------------------------------------- # Build-Cache je Quell-Hash und Build-Skript: ein unveraenderter Stand wird # nicht neu gebaut (Build rund 90 s), ein geaendertes Skript schon. IMAGE="$(python3 -I -c 'import json,sys; b=json.load(open(sys.argv[1]))["build_image"]; print(b["name"]+"@"+b["digest"])' "$MANIFEST")" SKRIPT="$ROOT/scripts/bentopdf-build.sh" SCHLUESSEL="$( (sha256sum "$CACHE/bentopdf-2.8.11-src.tar.gz"; sha256sum "$SKRIPT"; echo "$IMAGE") | sha256sum | cut -c1-16)" BCACHE="$CACHE/bentopdf-build-$SCHLUESSEL" if [ ! -d "$BCACHE/dist" ]; then log "baue BentoPDF im Container $IMAGE (Schluessel $SCHLUESSEL)" rm -rf "$BCACHE.part" mkdir -p "$BCACHE.part" docker run --rm \ -v "$CACHE/bentopdf-2.8.11-src.tar.gz:/in/src.tar.gz:ro" \ -v "$SKRIPT:/build.sh:ro" \ -v "$BCACHE.part:/out" \ "$IMAGE" bash /build.sh mv "$BCACHE.part" "$BCACHE" else log "BentoPDF-Build aus Cache ($SCHLUESSEL)" fi cp -r "$BCACHE/dist" "$TMP/pdf" # LibreOffice-WASM: soffice.wasm.gz 48.763.638 B ueber der Grenze; die 13 # Office-Werkzeuge sind per DISABLE_TOOLS aus. rm -rf "$TMP/pdf/libreoffice-wasm" # BentoPDFs _headers gilt nur im Root des Asset-Ordners; unter /pdf/ waere # es eine ausgelieferte Datei. Header setzt zentral der Worker/site/_headers. rm -f "$TMP/pdf/_headers" # WASM-Pakete nach pdf/wasm/ (Air-gapped-Konvention im BentoPDF-README, # Abschnitt "Manual Steps", Schritt 5). W="$TMP/pdf/wasm" mkdir -p "$W/pymupdf" "$W/gs" "$W/cpdf" "$W/ocr/core" "$W/ocr/lang-data" "$W/ocr/fonts" "$W/embedpdf" tar xzf "$CACHE/pymupdf-wasm-0.11.16.tgz" -C "$W/pymupdf" --strip-components=1 tar xzf "$CACHE/gs-wasm-0.1.1.tgz" -C "$W/gs" --strip-components=1 tar xzf "$CACHE/coherentpdf-2.5.5.tgz" -C "$W/cpdf" --strip-components=1 # Testdateien und Handbuch des cpdf-Pakets werden zur Laufzeit nie geladen. rm -rf "$W/cpdf/testinputs" "$W/cpdf/cpdfjsmanual.pdf" X="$TMP/x-tess"; mkdir -p "$X" tar xzf "$CACHE/tesseract.js-7.0.0.tgz" -C "$X" cp "$X/package/dist/worker.min.js" "$W/ocr/worker.min.js" cp "$X/package/LICENSE" "$W/ocr/LICENSE.tesseract.js" 2>/dev/null || true rm -rf "$X" tar xzf "$CACHE/tesseract.js-core-7.0.0.tgz" -C "$W/ocr/core" --strip-components=1 for l in deu eng; do X="$TMP/x-$l"; mkdir -p "$X" tar xzf "$CACHE/$l-1.0.0.tgz" -C "$X" # 4.0.0_best_int wie im README (Air-gapped, Schritt 1). cp "$X/package/4.0.0_best_int/$l.traineddata.gz" "$W/ocr/lang-data/$l.traineddata.gz" rm -rf "$X" done # Editor-Fallback-Schriften: Pfadschema /fonts-@1.0.0/fonts/ # (src/js/config/editor-fonts.ts, PACK). Nur die referenzierten Regular-Schnitte. for p in latin:NotoSans-Regular.ttf jp:NotoSansJP-Regular.otf kr:NotoSansKR-Regular.otf \ sc:NotoSansHans-Regular.otf tc:NotoSansHant-Regular.otf \ arabic:NotoNaskhArabic-Regular.ttf hebrew:NotoSansHebrew-Regular.ttf; do pack="${p%%:*}"; f="${p#*:}" X="$TMP/x-font-$pack"; mkdir -p "$X" tar xzf "$CACHE/embedpdf-fonts-$pack-1.0.0.tgz" -C "$X" mkdir -p "$W/embedpdf/fonts-$pack@1.0.0/fonts" cp "$X/package/fonts/$f" "$W/embedpdf/fonts-$pack@1.0.0/fonts/$f" cp "$X/package/LICENSE" "$W/embedpdf/fonts-$pack@1.0.0/LICENSE" rm -rf "$X" done # OCR-Textebene: deu/eng -> Noto Sans (src/js/config/font-mappings.ts). cp "$W/embedpdf/fonts-latin@1.0.0/fonts/NotoSans-Regular.ttf" "$W/ocr/fonts/NotoSans-Regular.ttf" log "pdf fertig" rm -rf "$OUT" mv "$TMP" "$OUT" for s in it-tools cyberchef pdf; do log "$s: $(find "$OUT/$s" -type f | wc -l) Dateien, $(du -sb "$OUT/$s" | cut -f1) B" done