Skip to content

Commit e237115

Browse files
committed
Introduce app metadata system, default to parsing Swift Bundler metadata
App metadata is static metadata loaded at app start-up. The Gtk backends now make use of this metadata when computing that app's identifier. Apps built with older Swift Bundler versions (or without Swift Bundler at all) still benefit from these changes as they can integrate their own metadata loading system (e.g. loading app identifier and version from an Info.plist on macOS).
1 parent f942fec commit e237115

File tree

6 files changed

+105
-18
lines changed

6 files changed

+105
-18
lines changed

Sources/AppKitBackend/AppKitBackend.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import SwiftCrossUI
33

44
extension App {
55
public typealias Backend = AppKitBackend
6+
7+
public var backend: AppKitBackend {
8+
AppKitBackend()
9+
}
610
}
711

812
public final class AppKitBackend: AppBackend {

Sources/Gtk3Backend/Gtk3Backend.swift

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import SwiftCrossUI
55

66
extension App {
77
public typealias Backend = Gtk3Backend
8+
9+
public var backend: Gtk3Backend {
10+
Gtk3Backend(appIdentifier: Self.metadata?.identifier)
11+
}
812
}
913

1014
extension SwiftCrossUI.Color {
@@ -35,14 +39,16 @@ public final class Gtk3Backend: AppBackend {
3539
/// precreated window until it gets 'created' via `createWindow`.
3640
var windows: [Window] = []
3741

38-
/// Creates a backend instance using the default app identifier `com.example.SwiftCrossUIApp`.
39-
convenience public init() {
40-
self.init(appIdentifier: "com.example.SwiftCrossUIApp")
42+
// A separate initializer to satisfy ``AppBackend``'s requirements.
43+
public convenience init() {
44+
self.init(appIdentifier: nil)
4145
}
4246

43-
public init(appIdentifier: String) {
47+
/// Creates a backend instance. If `appIdentifier` is `nil`, the default
48+
/// identifier `com.example.SwiftCrossUIApp` is used.
49+
public init(appIdentifier: String?) {
4450
gtkApp = Application(
45-
applicationId: appIdentifier,
51+
applicationId: appIdentifier ?? "com.example.SwiftCrossUIApp",
4652
flags: G_APPLICATION_HANDLES_OPEN
4753
)
4854
gtkApp.registerSession = true

Sources/GtkBackend/GtkBackend.swift

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ import SwiftCrossUI
55

66
extension App {
77
public typealias Backend = GtkBackend
8+
9+
public var backend: GtkBackend {
10+
GtkBackend(appIdentifier: Self.metadata?.identifier)
11+
}
812
}
913

1014
extension SwiftCrossUI.Color {
@@ -35,17 +39,20 @@ public final class GtkBackend: AppBackend {
3539
/// precreated window until it gets 'created' via `createWindow`.
3640
var windows: [Window] = []
3741

38-
/// Creates a backend instance using the default app identifier `com.example.SwiftCrossUIApp`.
39-
convenience public init() {
40-
self.init(appIdentifier: "com.example.SwiftCrossUIApp")
42+
// A separate initializer to satisfy ``AppBackend``'s requirements.
43+
public convenience init() {
44+
self.init(appIdentifier: nil)
4145
}
4246

43-
public init(appIdentifier: String) {
47+
/// Creates a backend instance. If `appIdentifier` is `nil`, the default
48+
/// identifier `com.example.SwiftCrossUIApp` is used.
49+
public init(appIdentifier: String?) {
4450
gtkApp = Application(
45-
applicationId: appIdentifier,
51+
applicationId: appIdentifier ?? "com.example.SwiftCrossUIApp",
4652
flags: G_APPLICATION_HANDLES_OPEN
4753
)
4854
gtkApp.registerSession = true
55+
print(appIdentifier)
4956
}
5057

5158
public func runMainLoop(_ callback: @escaping () -> Void) {

Sources/SwiftCrossUI/App.swift

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ public protocol App {
99
/// The app's observed state.
1010
associatedtype State: Observable
1111

12+
/// Metadata loaded at app start up. By default SwiftCrossUI attempts
13+
/// to load metadata inserted by Swift Bundler if present. Used by backends'
14+
/// default ``App/backend`` implementations if not `nil`.
15+
static var metadata: AppMetadata? { get }
16+
1217
/// The application's backend.
1318
var backend: Backend { get }
1419

@@ -26,9 +31,19 @@ public protocol App {
2631
/// this in your own code then something has gone very wrong...
2732
public var _forceRefresh: () -> Void = {}
2833

34+
/// Metadata embedded by Swift Bundler if present. Loaded at app start up.
35+
private var swiftBundlerAppMetadata: AppMetadata?
36+
2937
extension App {
38+
/// Metadata loaded at app start up.
39+
public static var metadata: AppMetadata? {
40+
swiftBundlerAppMetadata
41+
}
42+
3043
/// Runs the application.
3144
public static func main() {
45+
swiftBundlerAppMetadata = extractSwiftBundlerMetadata()
46+
3247
let app = Self()
3348
let _app = _App(app)
3449
_forceRefresh = {
@@ -38,16 +53,56 @@ extension App {
3853
}
3954
_app.run()
4055
}
56+
57+
private static func extractSwiftBundlerMetadata() -> AppMetadata? {
58+
guard let executable = Bundle.main.executableURL else {
59+
print("No executable url")
60+
return nil
61+
}
62+
63+
guard let data = try? Data(contentsOf: executable) else {
64+
print("warning: Executable failed to read self (to extract metadata)")
65+
return nil
66+
}
67+
68+
// Check if executable has Swift Bundler metadata magic bytes.
69+
let bytes = Array(data)
70+
guard bytes.suffix(8) == Array("SBUNMETA".utf8) else {
71+
print("no magic bytes")
72+
return nil
73+
}
74+
75+
let lengthStart = bytes.count - 16
76+
let jsonLength = parseBigEndianUInt64(startingAt: lengthStart, in: bytes)
77+
let jsonStart = lengthStart - Int(jsonLength)
78+
let jsonData = Data(bytes[jsonStart..<lengthStart])
79+
80+
do {
81+
return try JSONDecoder().decode(
82+
AppMetadata.self,
83+
from: jsonData
84+
)
85+
} catch {
86+
print("warning: Swift Bundler metadata present but couldn't be parsed")
87+
print(" -> \(error)")
88+
return nil
89+
}
90+
}
91+
92+
private static func parseBigEndianUInt64(
93+
startingAt startIndex: Int,
94+
in bytes: [UInt8]
95+
) -> UInt64 {
96+
bytes[startIndex..<(startIndex + 8)].withUnsafeBytes { pointer in
97+
let bigEndianValue = pointer.assumingMemoryBound(to: UInt64.self)
98+
.baseAddress!.pointee
99+
return UInt64(bigEndian: bigEndianValue)
100+
}
101+
}
41102
}
42103

43104
extension App where State == EmptyState {
44105
public var state: State {
45106
EmptyState()
46107
}
47108
}
48-
49-
extension App {
50-
public var backend: Backend {
51-
Backend()
52-
}
53-
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/// Metadata loaded at app start up.
2+
public struct AppMetadata: Codable {
3+
/// The app's reverse domain name identifier.
4+
public var identifier: String
5+
/// The app's version (generally a semantic version string).
6+
public var version: String
7+
8+
public init(identifier: String, version: String) {
9+
self.identifier = identifier
10+
self.version = version
11+
}
12+
13+
private enum CodingKeys: String, CodingKey {
14+
case identifier = "appIdentifier"
15+
case version = "appVersion"
16+
}
17+
}

Sources/SwiftCrossUI/Backend/AppBackend.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,6 @@ public protocol AppBackend {
4343
associatedtype Menu
4444
associatedtype Alert
4545

46-
init()
47-
4846
/// The default height of a table row excluding cell padding. This is a
4947
/// recommendation by the backend that SwiftCrossUI won't necessarily
5048
/// follow in all cases.

0 commit comments

Comments
 (0)