Files
clawdbot/apps/shared/ClawdbotKit/Sources/ClawdbotKit/ClawdbotKitResources.swift
gupsammy 29e9a574b0 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>
2026-01-08 19:24:20 +00:00

76 lines
2.7 KiB
Swift

import Foundation
public enum ClawdbotKitResources {
/// 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 {}