fix(macos): prevent crash from missing ClawdbotKit resources and Swift library

The macOS app was crashing in two scenarios:

1. Bundle.module crash (fixes #213): When the first tool event arrived,
   ToolDisplayRegistry tried to load config via ClawdbotKitResources.bundle,
   which used Bundle.module directly. In packaged apps, Bundle.module
   couldn't find the resource bundle at the expected path, causing a
   fatal assertion failure after ~40-80 minutes of runtime.

2. dyld crash (fixes #417): Swift 6.2 requires libswiftCompatibilitySpan.dylib
   but SwiftPM doesn't bundle it automatically, causing immediate crash on
   launch with "Library not loaded" error.

Changes:
- ClawdbotKitResources.swift: Replace direct Bundle.module access with a
  safe locator that checks multiple paths and falls back gracefully
- package-mac-app.sh: Copy ClawdbotKit_ClawdbotKit.bundle to Resources
- package-mac-app.sh: Copy libswiftCompatibilitySpan.dylib from Xcode
  toolchain to Frameworks

Tested on macOS 26.2 with Swift 6.2 - app launches and runs without crashes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gupsammy
2026-01-08 15:51:07 +05:30
committed by Peter Steinberger
parent ba19173b96
commit 29e9a574b0
2 changed files with 89 additions and 1 deletions

View File

@@ -1,5 +1,75 @@
import Foundation
public enum ClawdbotKitResources {
public static let bundle: Bundle = .module
/// Resource bundle for ClawdbotKit.
///
/// Locates the SwiftPM-generated resource bundle, checking multiple locations:
/// 1. Inside Bundle.main (packaged apps)
/// 2. Bundle.module (SwiftPM development/tests)
/// 3. Falls back to Bundle.main if not found (resource lookups will return nil)
///
/// This avoids a fatal crash when Bundle.module can't locate its resources
/// in packaged .app bundles where the resource bundle path differs from
/// SwiftPM's expectations.
public static let bundle: Bundle = locateBundle()
private static let bundleName = "ClawdbotKit_ClawdbotKit"
private static func locateBundle() -> Bundle {
// 1. Check inside Bundle.main (packaged apps copy resources here)
if let mainResourceURL = Bundle.main.resourceURL {
let bundleURL = mainResourceURL.appendingPathComponent("\(bundleName).bundle")
if let bundle = Bundle(url: bundleURL) {
return bundle
}
}
// 2. Check Bundle.main directly for embedded resources
if Bundle.main.url(forResource: "tool-display", withExtension: "json") != nil {
return Bundle.main
}
// 3. Try Bundle.module (works in SwiftPM development/tests)
// Wrap in a function to defer the fatalError until actually called
if let moduleBundle = loadModuleBundleSafely() {
return moduleBundle
}
// 4. Fallback: return Bundle.main (resource lookups will return nil gracefully)
return Bundle.main
}
private static func loadModuleBundleSafely() -> Bundle? {
// Bundle.module is generated by SwiftPM and will fatalError if not found.
// We check likely locations manually to avoid the crash.
let candidates: [URL?] = [
Bundle.main.resourceURL,
Bundle.main.bundleURL,
Bundle(for: BundleLocator.self).resourceURL,
Bundle(for: BundleLocator.self).bundleURL,
]
for candidate in candidates {
guard let baseURL = candidate else { continue }
// Direct path
let directURL = baseURL.appendingPathComponent("\(bundleName).bundle")
if let bundle = Bundle(url: directURL) {
return bundle
}
// Inside Resources/
let resourcesURL = baseURL
.appendingPathComponent("Resources")
.appendingPathComponent("\(bundleName).bundle")
if let bundle = Bundle(url: resourcesURL) {
return bundle
}
}
return nil
}
}
// Helper class for bundle lookup via Bundle(for:)
private final class BundleLocator {}

View File

@@ -221,6 +221,15 @@ if [ -d "$SPARKLE_FRAMEWORK_PRIMARY" ]; then
chmod -R a+rX "$APP_ROOT/Contents/Frameworks/Sparkle.framework"
fi
echo "📦 Copying Swift 6.2 compatibility libraries"
SWIFT_COMPAT_LIB="$(xcode-select -p)/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift-6.2/macosx/libswiftCompatibilitySpan.dylib"
if [ -f "$SWIFT_COMPAT_LIB" ]; then
cp "$SWIFT_COMPAT_LIB" "$APP_ROOT/Contents/Frameworks/"
chmod +x "$APP_ROOT/Contents/Frameworks/libswiftCompatibilitySpan.dylib"
else
echo "WARN: Swift compatibility library not found at $SWIFT_COMPAT_LIB (continuing)" >&2
fi
echo "🖼 Copying app icon"
cp "$ROOT_DIR/apps/macos/Sources/Clawdbot/Resources/Clawdbot.icns" "$APP_ROOT/Contents/Resources/Clawdbot.icns"
@@ -228,6 +237,15 @@ echo "📦 Copying device model resources"
rm -rf "$APP_ROOT/Contents/Resources/DeviceModels"
cp -R "$ROOT_DIR/apps/macos/Sources/Clawdbot/Resources/DeviceModels" "$APP_ROOT/Contents/Resources/DeviceModels"
echo "📦 Copying ClawdbotKit resources"
CLAWDBOTKIT_BUNDLE="$(build_path_for_arch "$PRIMARY_ARCH")/$BUILD_CONFIG/ClawdbotKit_ClawdbotKit.bundle"
if [ -d "$CLAWDBOTKIT_BUNDLE" ]; then
rm -rf "$APP_ROOT/Contents/Resources/ClawdbotKit_ClawdbotKit.bundle"
cp -R "$CLAWDBOTKIT_BUNDLE" "$APP_ROOT/Contents/Resources/ClawdbotKit_ClawdbotKit.bundle"
else
echo "WARN: ClawdbotKit resource bundle not found at $CLAWDBOTKIT_BUNDLE (continuing)" >&2
fi
RELAY_DIR="$APP_ROOT/Contents/Resources/Relay"
if [[ "${SKIP_GATEWAY_PACKAGE:-0}" != "1" ]]; then