Bauen und testen
Bauen
git clone https://github.com/fre-ms/zotLook.git
cd zotLook
./build.shbuild.sh kompiliert den Helfer (qlpreview) für beide Architekturen und signiert sie — swiftc signiert die arm64-Scheibe ad hoc, die x86_64-Scheibe aber nicht, und lipo signiert die Hülle nicht, ohne diesen Schritt meldet sich das Ergebnis also als unsigniert. Danach schnürt es die .xpi und schreibt update.json passend neu: Version, Download-Adresse und SHA-256 sämtlich aus dem Manifest und der eben gebauten Datei abgeleitet.
Ein Freigabe-Build braucht deshalb macOS mit den Xcode Command Line Tools. Jener Helfer ist das einzige kompilierte Stück im Paket: Der Windows-Weg steuert QuickLooks Pipe über js-ctypes an und liefert kein eigenes Binary mit.
Dasselbe Skript läuft auch unter Linux, wo es keine Swift-Toolchain gibt. Es schnürt dann alles Übrige, was der Linux-Weg auch braucht, und lässt update.json unangetastet — ein Paket ohne den Helfer ist zum lokalen Installieren und Prüfen in Ordnung, seine Prüfsumme zu veröffentlichen würde aber macOS-Installationen auf einen Stand aktualisieren, dem ihre Quick-Look-Ansicht fehlt.
Die Entwicklungsschleife
Für eine einzeilige Änderung eine XPI zu bauen, ist langsamer als nötig — und es steckt eine Falle darin: Ändert sich die Versionsnummer nicht, behält Zotero die Kopie, die es schon hat. Der erste Neustart zeigt dann das alte Plugin und erst der zweite das neue. Das hat mehr Zeit gekostet als jeder Fehler.
Zotero kann ein Plugin auch aus einem Verzeichnis laden — über eine Datei im extensions/ des Profils, die nach der Plugin-Kennung heißt und den Pfad enthält:
node tool/dev.mjs # aus dem Arbeitsbaum installieren, neu starten, beobachten
node tool/dev.mjs --install # nur installieren
node tool/dev.mjs --restore # die verpackte Kopie zurücklegenEs wird nichts gepackt und nichts kopiert: Ein Neustart nimmt, was gerade in addon/ steht. Unter macOS baut die Installation zuerst den Quick-Look-Helfer nach addon/ (build.sh --helper), denn build.sh entfernt ihn, sobald die XPI ihn enthält — und ein Baum ohne ihn fällt auf qlmanage zurück, dessen Panel die Links des Kontaktbogens weder ans System weiterreicht noch sich beim Übergeben schließt. Die Vorschau sieht dann richtig aus, und die Links tun nichts. Der Beobachter startet Zotero neu, sobald sich dort etwas ändert, und zwar mit --purgecaches. Das ist wesentlich: Zotero hält übersetzte Skripte über Neustarts hinweg vor, und bootstrap.js wird von Zotero selbst zwischengespeichert, nicht vom Plugin — ohne das läuft nach einem Neustart womöglich der Lader von gestern gegen die Module von heute.
Die verpackte Kopie wird beiseitegelegt statt gelöscht — in der Profilwurzel, nicht in extensions/, denn dieses Verzeichnis gehört Zotero, das dort alles Wegräumt, was es nicht kennt. --restore legt sie zurück, und das Profil steht wieder genau wie zuvor. Außerdem wird extensions.lastAppBuildId geleert, ohne das nichts davon funktioniert: Zotero liest extensions/ nur dann neu ein, wenn der vermerkte Build nicht mehr passt — eine Proxy-Datei neben einem Eintrag, der noch das Paket nennt, wird schlicht nicht angesehen. Das Skript aktiviert das Plugin außerdem in extensions.json wieder: Ein einmal deaktiviertes bleibt deaktiviert, wie auch immer es installiert wird — eine richtige Installation sieht dann aus wie eine kaputte.
--install und --restore wirken auf Dateien, die Zotero beim Start liest, und auf eine Einstellung, die es beim Beenden neu schreibt — schließen Sie Zotero vor beidem. Unter einem laufenden sehen sie aus, als hätten sie gewirkt, und ändern nichts. Der Beobachter hat das Problem nicht, er schließt Zotero selbst. Und solange die Entwicklungs-Installation steht, lädt jeder Start den Arbeitsbaum, auch der über das Programmsymbol; --restore legt die verpackte Kopie zurück.
--profile nimmt ein anderes Profil als das, welches profiles.ini nennt; ZOTERO_BIN ein Zotero an ungewöhnlicher Stelle, ZOTERO_QUIT einen anderen Weg, es zu schließen, als den der Plattform, und ZOTERO_PROBE einen Befehl, der mit 0 endet, solange es läuft. Der Neustart wartet, bis dieser das Ende meldet, bevor er etwas startet: open -a auf ein noch schließendes Zotero aktiviert es nur und bricht das Beenden ab — Letzteres ist zugleich, was test/dev.test.mjs erlaubt, die ganze Schleife gegen Attrappen zu treiben, statt das Zotero dessen zu schließen, der die Tests laufen lässt.
Es ist kein Hot Reload, und das ist eine Entscheidung, kein Versäumnis. Ein Plugin im laufenden Zotero neu zu laden, verlangt Geckos Remote-Debugging-Protokoll oder ein Plugin, das einen Endpunkt dafür registriert — Letzteres nutzt zotero-plugin-scaffold über einen zotero://ztoolkit-debug-Handler, der aus zotero-plugin-toolkit stammt und nicht aus Zotero. Zoteros eigene zotero://-Endpunkte sind attachment, data, report, select, debug, pdf.js und open-pdf; eine Brücke ist nicht darunter. Das Scaffold allein für den Neustart hereinzuholen, hieße sechzehn weitere Abhängigkeiten gegen einen Packvorgang von sechs Sekunden, von denen der Swift-Übersetzer keine zwei ausmacht.
Aufbau des Repositorys
addon/ wandert unverändert ins XPI; das Plugin, wie Zotero es sieht
native/ Swift-Helfer, von build.sh kompiliert statt mitgeliefert
asset/ Bilder
doc/ diese Dokumentation
test/ Testsuite
build/ das geschnürte .xpi; von git ignoriert
Zotero verlangt manifest.json, bootstrap.js und locale/ in der Wurzel des XPI — deshalb wird addon/ von innen heraus geschnürt und nicht als Ordner.
update.json bleibt in der Wurzel des Repositorys, statt nach build/ zu wandern: Es ist kein Bauartefakt, sondern eine veröffentlichte Datei. Der Doku-Deploy legt sie auf zotlook.org, wo Kopien ab 1.4.2 sie abfragen, und ältere Kopien fragen sie auf main ab.
Tests
npm install # linkedom als Ersatz für Geckos DOM, und eslint
npm test # erst Lint, dann die SuitenEin verlorengegangener Funktionsparameter hinterlässt einen Bezeichner, der noch parst und zur Laufzeit wirft. node --check sieht das nicht, und sämtliche Suiten übersahen es so lange, wie die Funktion darum herum durch eine Attrappe ersetzt war.
Die Suiten führen den Quelltext des Plugins in einem Bereich aus, in dem Zoteros Sandbox-Globale nachgebildet sind. Parsen, Bereinigen, Tastatur, Menü und EPUB-Kette lassen sich so prüfen, ohne Zotero zu starten. Das Zeichnen wird über eine Attrappe angesteuert, die dasselbe Zweischritt-Protokoll spricht wie der echte Worker.
Geprüft wird außerdem, was sonst lautlos scheitert: dass update.json zum Manifest passt, dass beide Sprachpakete genau die Texte definieren, die der Code anfordert, und dass der Quelltext des Renderers noch jede Anforderung trägt, die pdf.js an ihn stellt — jede davon durch Zusehen beim Scheitern gefunden, und keine davon meldet sich, wenn sie fehlt.
Das Plugin hat keine Laufzeit-Abhängigkeiten; linkedom ist reine Entwicklungs-Abhängigkeit und wird nie mitgeliefert.
CI
Läuft bei jedem Push und jedem Pull Request.
Die Suiten laufen unter Linux, wo sie plattformunabhängig sind, und unter Windows, wo ein Pfadname kein Pfad ist und die Annahmen abweichen. Der Freigabe-Build läuft unter macOS, weil swiftc, lipo und codesign nur dort zu Hause sind; er prüft, dass das XPI jede Datei enthält, die Zotero in seiner Wurzel braucht, und dass der Helfer universal und signiert ist. Die Suiten laufen auf jenem Job ein zweites Mal, wo ein frisch gebautes XPI vorliegt — so läuft auch die Prüfung, dass update.json die tatsächlich erzeugte Datei beschreibt.
Das XPI ist nicht bytegleich reproduzierbar — Zip vermerkt Änderungszeiten — deshalb vergleicht die CI seine Prüfsumme nicht gegen das eingecheckte update.json. Verglichen wird die Version, und die ist es, die tatsächlich veraltet.
Diese Dokumentation
cd doc && ./build.sh
python3 doc/offline/smoketest.py site/en site/deBeide Sprachprojekte werden nach site/{en,de} gerendert, die Offline-Reparaturen angewandt und je Sprache ein verlinktes PDF mit Typst gebaut.
Die Seite steht auf dem easyQDA-quarto-theme, das nach doc/_theme/ vendort ist, damit die Dokumentation ohne Nachbar-Checkout baut. doc/update-theme.sh holt eine neuere Fassung herein.
Quarto ist gepinnt, und zwar auf dieselbe Fassung wie die übrigen Ökosystem-Seiten, damit alle auf derselben Version rendern. Hebungen sind absichtsvoll: mit ALLOW_UNPINNED_QUARTO=1 bauen, den Smoketest laufen lassen, eine Seite ansehen, dann den Pin verschieben.
Die Seitenleisten der beiden Sprachen müssen parallel bleiben — gen_langmap.py leitet daraus die Seite-zu-Seite-Zuordnung des Sprachumschalters ab und lässt den Build scheitern, wenn sie es nicht mehr sind.