From a681b2e3321e6833ccbd8c6ab6b3f7d326f68339 Mon Sep 17 00:00:00 2001 From: Roland Rabien Date: Thu, 16 Jul 2026 18:18:43 -0700 Subject: [PATCH] Add crash reporting (bundle CrashReporter, strip, symbols, upload) - register plugin + ship Installer/crashreporter.json - installer bundles the shared CrashReporter (mac pkg + component-plist + reporter-scripts promote-if-newer; win .iss shared component) and the JSON - strip shipped mac binaries; emit dSYM (Xcode) and PDB (/Zi /DEBUG) in Release - launchCrashReporterOnce() from the processor on first instance - upload dSYM/PDB symbols to the crash site from release.yaml - bump to 1.0.33 --- .github/workflows/release.yaml | 1 + CMakeLists.txt | 17 +++ Changelist.txt | 3 + Installer/build.sh | 110 ++++++++++++++++++- Installer/crashreporter.json | 6 + Installer/macOS/distribution.xml | 11 ++ Installer/macOS/reporter-scripts/postinstall | 38 +++++++ Installer/win/Wavetable.iss | 8 ++ VERSION | 2 +- plugin/Source/PluginProcessor.cpp | 25 +++++ 10 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 Installer/crashreporter.json create mode 100755 Installer/macOS/reporter-scripts/postinstall diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 42df63a..c48a2a8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -53,6 +53,7 @@ jobs: AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }} AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }} AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }} + SYMBOL_API_KEY: ${{ secrets.SYMBOL_API_KEY }} - name: Upload Artifact uses: actions/upload-artifact@v4 diff --git a/CMakeLists.txt b/CMakeLists.txt index 4a8d37b..7504482 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -213,6 +213,7 @@ if (APPLE) XCODE_ATTRIBUTE_CLANG_LINK_OBJC_RUNTIME NO #XCODE_ATTRIBUTE_DEPLOYMENT_POSTPROCESSING[variant=Release] YES XCODE_ATTRIBUTE_ONLY_ACTIVE_ARCH[variant=Debug] "YES" + XCODE_ATTRIBUTE_DEBUG_INFORMATION_FORMAT[variant=Release] "dwarf-with-dsym" ) if (NOT t STREQUAL "All") target_compile_options(${tgt} PRIVATE @@ -237,6 +238,22 @@ if (WIN32) set_target_properties(${tgt} PROPERTIES LINK_FLAGS "/ignore:4099") endif() endforeach() + + # Generate debug symbols (PDBs) for Release builds so crashes can be symbolicated. + # /Zi emits full debug info; /DEBUG produces the .pdb; /OPT:REF and /OPT:ICF + # restore the size optimisations that /DEBUG otherwise disables. + foreach(t ${FORMATS} "CLAP" "") + set(tgt ${CMAKE_PROJECT_NAME}) + if (NOT t STREQUAL "") + set(tgt ${tgt}_${t}) + endif() + if (TARGET ${tgt}) + target_compile_options(${tgt} PRIVATE "$<$:/Zi>") + target_link_options(${tgt} PRIVATE "$<$:/DEBUG>") + target_link_options(${tgt} PRIVATE "$<$:/OPT:REF>") + target_link_options(${tgt} PRIVATE "$<$:/OPT:ICF>") + endif() + endforeach() endif() if(UNIX AND NOT APPLE) diff --git a/Changelist.txt b/Changelist.txt index e2e08b4..b3babb2 100644 --- a/Changelist.txt +++ b/Changelist.txt @@ -1,3 +1,6 @@ +1.0.33: +- Added crash reporting + 1.0.32: - Fixed fix wavetable menus - Fixed deadlock diff --git a/Installer/build.sh b/Installer/build.sh index 6e2da81..e559fe2 100755 --- a/Installer/build.sh +++ b/Installer/build.sh @@ -21,6 +21,42 @@ fi VERSION=$(cat "$PROJECT_ROOT/VERSION") +# +# Crash reporting: bundle the latest CrashReporter app + this plugin's +# registration JSON, and upload debug symbols so crashes can be symbolicated. +# SYMBOL_API_KEY - CI secret, authorises symbol upload for this plugin. +# The CrashReporter download is a public distribution channel (no key needed). +# +CRASH_BASE="https://crashreports.rabiensoftware.com" + +# Native (non-MSYS) curl on Windows can't read Git-Bash paths like /d/a/..., +# so translate to a Windows path there. No-op on macOS/Linux. +curl_path () { + if command -v cygpath >/dev/null 2>&1; then cygpath -m "$1"; else printf '%s' "$1"; fi +} + +# Download the latest CrashReporter build for a platform. Non-fatal: if none is +# published yet we still ship the registration JSON so crashes register. +fetch_reporter () { # $1 platform, $2 output file + if curl -fsSL "$CRASH_BASE/reporter/latest/?platform=$1" -o "$(curl_path "$2")"; then + return 0 + fi + echo "WARNING: could not fetch CrashReporter for $1 (none published yet?)" + return 1 +} + +# Upload a symbols archive for this plugin/version. Skipped if the key is unset. +upload_symbols () { # $1 platform, $2 zip file + if [ -z "${SYMBOL_API_KEY:-}" ]; then echo "SYMBOL_API_KEY not set — skipping symbol upload"; return 0; fi + if [ ! -f "$2" ]; then echo "No symbol archive $2 — skipping"; return 0; fi + echo "Uploading $1 symbols for $VERSION" + # Non-fatal: a symbol-upload failure must never break a release build. + curl -fsS -H "X-API-Key: $SYMBOL_API_KEY" \ + -F "platform=$1" -F "version=$VERSION" -F "files[]=@$(curl_path "$2")" \ + "$CRASH_BASE/symbols/" || echo "WARNING: symbol upload failed" + echo +} + # # Reset staging # @@ -95,6 +131,15 @@ if [ "$PLATFORM" = "macOS" ]; then cp -R "$PROJECT_ROOT/plugin/Resources/WavetablesFLAC" "$STAGE/resources/Library/Audio/Presets/$VENDOR/$PLUGIN/Wavetables" find "$STAGE/resources" -name ".DS_Store" -delete + # Strip symbols from the shipped binaries so end-user crash logs are NOT + # symbolicated locally by macOS — the server symbolicates them from the dSYMs + # (built below from the unstripped products in $ART_DIR). strip preserves the + # Mach-O UUID, so the dSYM still matches. Before codesign. + strip -x "$STAGE/vst/$PLUGIN.vst/Contents/MacOS/$PLUGIN" + strip -x "$STAGE/vst3/$PLUGIN.vst3/Contents/MacOS/$PLUGIN" + strip -x "$STAGE/au/$PLUGIN.component/Contents/MacOS/$PLUGIN" + strip -x "$STAGE/clap/$PLUGIN.clap/Contents/MacOS/$PLUGIN" + if [ -n "${APPLICATION:-}" ]; then codesign -s "$DEV_APP_ID" --options=runtime --timestamp --force -v "$STAGE/vst/$PLUGIN.vst" codesign -s "$DEV_APP_ID" --options=runtime --timestamp --force -v "$STAGE/vst3/$PLUGIN.vst3" @@ -136,6 +181,38 @@ if [ "$PLATFORM" = "macOS" ]; then --scripts "$PROJECT_ROOT/Installer/macOS/scripts" \ "$PKG_DIR/resources.pkg" + # CrashReporter component: latest signed CrashReporter.app + this plugin's + # registration JSON. Staged under .incoming and promoted by the postinstall + # only if strictly newer (shared component, never downgraded). + REP_STAGE="$PROJECT_ROOT/Installer/macOS/bin/reporter" + REP_ROOT="$REP_STAGE/Library/Application Support/Rabien Software/Crash Reporter" + rm -Rf "$REP_STAGE" + mkdir -p "$REP_ROOT/Plugins" "$REP_ROOT/.incoming" + if fetch_reporter mac "$REP_STAGE/CrashReporter_Mac.zip"; then + ( cd "$REP_STAGE" && unzip -qo CrashReporter_Mac.zip && rm CrashReporter_Mac.zip ) + mv "$REP_STAGE/CrashReporter.app" "$REP_ROOT/.incoming/" + fi + cp "$PROJECT_ROOT/Installer/crashreporter.json" "$REP_ROOT/Plugins/wavetable.json" + find "$REP_STAGE" -name ".DS_Store" -delete + + if [ -n "${APPLICATION:-}" ] && [ -d "$REP_ROOT/.incoming/CrashReporter.app" ]; then + codesign -s "$DEV_APP_ID" --options=runtime --timestamp --force -v "$REP_ROOT/.incoming/CrashReporter.app" + fi + + chmod +x "$PROJECT_ROOT/Installer/macOS/reporter-scripts/postinstall" + + # Disable bundle relocation so the installer installs to our staged path + # instead of redirecting to a CrashReporter.app found elsewhere via Spotlight. + COMP_PLIST="$PROJECT_ROOT/Installer/macOS/bin/reporter-component.plist" + pkgbuild --analyze --root "$REP_STAGE" "$COMP_PLIST" + /usr/libexec/PlistBuddy -c "Set :0:BundleIsRelocatable false" "$COMP_PLIST" 2>/dev/null || true + + pkgbuild --root "$REP_STAGE" --install-location "/" \ + --identifier "${BUNDLE_BASE}.crashreporter.pkg" --version "$VERSION" \ + --component-plist "$COMP_PLIST" \ + --scripts "$PROJECT_ROOT/Installer/macOS/reporter-scripts" \ + "$PKG_DIR/reporter.pkg" + # productbuild — combine into one signed installer cp "$PROJECT_ROOT/Installer/EULA.rtf" "$PKG_DIR/EULA.rtf" cp "$PROJECT_ROOT/Installer/macOS/welcome.txt" "$PKG_DIR/welcome.txt" @@ -172,7 +249,18 @@ if [ "$PLATFORM" = "macOS" ]; then cp "$PKG_OUT" "$PROJECT_ROOT/bin/" - # Symbols zip + # Symbols zip. Some Xcode/CMake combos don't emit .dSYMs next to the product, + # so generate any that are missing straight from the built binaries. + for pair in "VST:$PLUGIN.vst" "VST3:$PLUGIN.vst3" "AU:$PLUGIN.component" "CLAP:$PLUGIN.clap"; do + d="${pair%%:*}"; b="${pair#*:}" + dsym="$ART_DIR/$d/$b.dSYM" + bin="$ART_DIR/$d/$b/Contents/MacOS/$PLUGIN" + if [ ! -d "$dsym" ] && [ -f "$bin" ]; then + echo "Generating dSYM for $d/$b" + dsymutil "$bin" -o "$dsym" || true + fi + done + cd "$ART_DIR" zip -r "$PROJECT_ROOT/bin/Symbols_Mac.zip" \ AU/$PLUGIN.component.dSYM \ @@ -180,6 +268,8 @@ if [ "$PLATFORM" = "macOS" ]; then VST3/$PLUGIN.vst3.dSYM \ CLAP/$PLUGIN.clap.dSYM 2>/dev/null || true + upload_symbols mac "$PROJECT_ROOT/bin/Symbols_Mac.zip" + ############################################################ # Linux — cpack DEB (VST + VST3 + LV2 + CLAP + Resources) ############################################################ @@ -210,6 +300,14 @@ else cp -R "$ART_DIR/VST3/$PLUGIN.vst3" "$STAGE/VST3/" cp -R "$ART_DIR/CLAP/$PLUGIN.clap" "$STAGE/CLAP/" + # CrashReporter: latest signed build + this plugin's registration JSON. + REP_DIR="$STAGE/CrashReporter" + rm -Rf "$REP_DIR"; mkdir -p "$REP_DIR" + if fetch_reporter win "$REP_DIR/CrashReporter_Win.zip"; then + ( cd "$REP_DIR" && 7z x -y CrashReporter_Win.zip >/dev/null && rm CrashReporter_Win.zip ) + fi + cp "$PROJECT_ROOT/Installer/crashreporter.json" "$REP_DIR/wavetable.json" + # # Sign binaries via Microsoft Trusted Signing. # Required env: AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET. @@ -272,4 +370,14 @@ else fi cp "$EXE_OUT" "$PROJECT_ROOT/bin/" + + # Symbols zip — PDBs for crash symbolication. + SYM_DIR="$STAGE/symbols" + rm -Rf "$SYM_DIR"; mkdir -p "$SYM_DIR/VST" "$SYM_DIR/VST3" "$SYM_DIR/CLAP" + cp "$ART_DIR/VST/$PLUGIN.pdb" "$SYM_DIR/VST/" 2>/dev/null || true + cp "$ART_DIR/VST3/$PLUGIN.pdb" "$SYM_DIR/VST3/" 2>/dev/null || true + cp "$ART_DIR/CLAP/$PLUGIN.pdb" "$SYM_DIR/CLAP/" 2>/dev/null || true + ( cd "$SYM_DIR" && 7z a "$PROJECT_ROOT/bin/Symbols_Win.zip" VST VST3 CLAP ) + + upload_symbols win "$PROJECT_ROOT/bin/Symbols_Win.zip" fi diff --git a/Installer/crashreporter.json b/Installer/crashreporter.json new file mode 100644 index 0000000..cc52a9c --- /dev/null +++ b/Installer/crashreporter.json @@ -0,0 +1,6 @@ +{ + "name": "Wavetable", + "pluginID": "com.socalabs.wavetable", + "crashUrl": "https://crashreports.rabiensoftware.com/post/", + "apiKey": "da8b3bc4e5fcb9b5ba8b5960d0ef805b62d4097b956082c87fcf82cab40cc2c8" +} diff --git a/Installer/macOS/distribution.xml b/Installer/macOS/distribution.xml index 63aa3fd..6846ef7 100644 --- a/Installer/macOS/distribution.xml +++ b/Installer/macOS/distribution.xml @@ -13,6 +13,7 @@ + + + + + vst.pkg vst3.pkg au.pkg clap.pkg resources.pkg + reporter.pkg diff --git a/Installer/macOS/reporter-scripts/postinstall b/Installer/macOS/reporter-scripts/postinstall new file mode 100755 index 0000000..9ae4a00 --- /dev/null +++ b/Installer/macOS/reporter-scripts/postinstall @@ -0,0 +1,38 @@ +#!/bin/bash +# +# CrashReporter is a SHARED component across all SocaLabs/Rabien plugins, so we +# never downgrade it and never remove it. The pkg payload stages the incoming +# app under .incoming; here we promote it to the live location only if it is +# strictly newer than what's already installed (or nothing is installed yet). +# +set -e + +BASE="/Library/Application Support/Rabien Software/Crash Reporter" +NEW="$BASE/.incoming/CrashReporter.app" +LIVE="$BASE/CrashReporter.app" +PB="/usr/libexec/PlistBuddy" + +version_of () { # $1 = .app path -> prints CFBundleShortVersionString or 0 + if [ -d "$1" ]; then + "$PB" -c "Print :CFBundleShortVersionString" "$1/Contents/Info.plist" 2>/dev/null || echo 0 + else + echo 0 + fi +} + +if [ -d "$NEW" ]; then + newver="$(version_of "$NEW")" + livever="$(version_of "$LIVE")" + + # Promote if nothing installed, or the incoming build is strictly newer. + highest="$(printf '%s\n%s\n' "$livever" "$newver" | sort -V | tail -1)" + if [ ! -d "$LIVE" ] || { [ "$highest" = "$newver" ] && [ "$newver" != "$livever" ]; }; then + rm -rf "$LIVE" + mv "$NEW" "$LIVE" + fi +fi + +# Always clear the staging area. +rm -rf "$BASE/.incoming" + +exit 0 diff --git a/Installer/win/Wavetable.iss b/Installer/win/Wavetable.iss index 6f3e732..133a24a 100644 --- a/Installer/win/Wavetable.iss +++ b/Installer/win/Wavetable.iss @@ -56,6 +56,7 @@ Name: "vst"; Description: "VST plug-in"; Types: full cu Name: "vst3"; Description: "VST3 plug-in"; Types: full custom; Flags: checkablealone Name: "clap"; Description: "CLAP plug-in"; Types: full custom; Flags: checkablealone Name: "resources"; Description: "Factory wavetables and presets"; Types: full custom; Flags: fixed +Name: "crashreporter"; Description: "Crash reporter (shared component, only updated if newer)"; Types: full custom; Flags: checkablealone [InstallDelete] Type: files; Name: "{commoncf64}\VST2\Wavetable.dll"; Components: vst @@ -74,3 +75,10 @@ Source: "bin\CLAP\Wavetable.clap"; DestDir: "{commoncf64}\CLAP"; ; Presets are flattened by Installer/build.sh into Installer/_flat_presets/. Source: "..\_flat_presets\*.xml"; DestDir: "{commonappdata}\SocaLabs\Wavetable\Presets\"; Flags: ignoreversion; Components: resources Source: "..\..\plugin\Resources\WavetablesFLAC\*.wt2048"; DestDir: "{commonappdata}\SocaLabs\Wavetable\Wavetables\"; Flags: ignoreversion recursesubdirs createallsubdirs; Components: resources + +; CrashReporter app → C:\Program Files\Rabien Software\Crash Reporter, plus this +; plugin's registration JSON → C:\ProgramData\Rabien Software\Crash Reporter\Plugins. +; Shared across plugins: the app is only updated if newer and never removed on +; uninstall; the registration JSON is always installed and never removed. +Source: "bin\CrashReporter\CrashReporter.exe"; DestDir: "{commonpf}\Rabien Software\Crash Reporter"; Flags: skipifsourcedoesntexist uninsneveruninstall; Components: crashreporter +Source: "bin\CrashReporter\wavetable.json"; DestDir: "{commonappdata}\Rabien Software\Crash Reporter\Plugins"; Flags: ignoreversion uninsneveruninstall diff --git a/VERSION b/VERSION index 08a69b5..c1cf2f9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.32 \ No newline at end of file +1.0.33 \ No newline at end of file diff --git a/plugin/Source/PluginProcessor.cpp b/plugin/Source/PluginProcessor.cpp index 1d6d488..00f6604 100644 --- a/plugin/Source/PluginProcessor.cpp +++ b/plugin/Source/PluginProcessor.cpp @@ -2,6 +2,29 @@ #include "PluginEditor.h" #include "WavetableVoice.h" +#include + +// If the shared CrashReporter is installed, launch it once per process (on the +// first plugin instance) so it can scan and upload any crash from last session. +static void launchCrashReporterOnce() +{ + static std::once_flag flag; + std::call_once (flag, [] + { + #if JUCE_MAC + juce::File app ("/Library/Application Support/Rabien Software/Crash Reporter/CrashReporter.app"); + #elif JUCE_WINDOWS + auto app = juce::File::getSpecialLocation (juce::File::globalApplicationsDirectory) + .getChildFile ("Rabien Software").getChildFile ("Crash Reporter").getChildFile ("CrashReporter.exe"); + #else + juce::File app; + #endif + + if (app.exists()) + juce::Process::openDocument (app.getFullPathName(), {}); + }); +} + static juce::String subTextFunction (const gin::Parameter&, float v) { switch (int (v)) @@ -563,6 +586,8 @@ WavetableAudioProcessor::WavetableAudioProcessor() fireAmp (FXBaseCallback ([this] { return gin::Processor::getSampleRate(); })), grindAmp (FXBaseCallback ([this] { return gin::Processor::getSampleRate(); })) { + launchCrashReporterOnce(); + // One-time migration of any user presets from the pre-installer location. // Factory presets now live in systemResourceRoot()/Presets and are surfaced // via getFactoryProgramDirectories(). User saves go to userResourceRoot()/Presets.