84 lines
3.2 KiB
Swift
84 lines
3.2 KiB
Swift
import Foundation
|
|
|
|
public enum OpenClawKitResources {
|
|
/// Resource bundle for OpenClawKit.
|
|
///
|
|
/// 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 = "OpenClawKit_OpenClawKit"
|
|
|
|
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 }
|
|
|
|
// SwiftPM often places the resource bundle next to (or near) the test runner bundle,
|
|
// not inside it. Walk up a few levels and check common container paths.
|
|
var roots: [URL] = []
|
|
roots.append(baseURL)
|
|
roots.append(baseURL.appendingPathComponent("Resources"))
|
|
roots.append(baseURL.appendingPathComponent("Contents/Resources"))
|
|
|
|
var current = baseURL
|
|
for _ in 0 ..< 5 {
|
|
current = current.deletingLastPathComponent()
|
|
roots.append(current)
|
|
roots.append(current.appendingPathComponent("Resources"))
|
|
roots.append(current.appendingPathComponent("Contents/Resources"))
|
|
}
|
|
|
|
for root in roots {
|
|
let bundleURL = root.appendingPathComponent("\(bundleName).bundle")
|
|
if let bundle = Bundle(url: bundleURL) {
|
|
return bundle
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// Helper class for bundle lookup via Bundle(for:)
|
|
private final class BundleLocator {}
|