Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
ed1039b
adopt swift-log
kaascevich Dec 29, 2025
439952b
undo a test change I made
kaascevich Dec 29, 2025
378ce3a
documentation and formatting fixups
kaascevich Dec 29, 2025
f5bf14b
cache logs by `extractSwiftBundlerMetadata()` and output them once th…
kaascevich Dec 29, 2025
1c9cee8
add a `logHandler(label:metadata:)` requirement to `App` and use it
kaascevich Dec 29, 2025
cca959a
use swift-log in the backends
kaascevich Dec 29, 2025
ca62da6
use `InMemoryLogHandler` for `extractSwiftBundlerMetadata()`
kaascevich Dec 30, 2025
ccfba39
refactor `extractSwiftBundlerMetadata()` to be `throws`
kaascevich Dec 30, 2025
be915af
documentation fixups
kaascevich Dec 30, 2025
5d0bdfa
fix comment casing
kaascevich Dec 30, 2025
150a6cd
fix swift-bundler metadata error log not using `localizedDescription`
kaascevich Dec 30, 2025
41b3a37
Merge branch 'main' into swift-log
kaascevich Dec 31, 2025
87582a2
convert remaining `print` calls into `logger` calls
kaascevich Dec 31, 2025
8e6200e
remove `logger.notice` call only in place for debugging
kaascevich Jan 2, 2026
d494666
change the minimum swift-log version to 1.7.0 (for Swift 5.10 compat)
kaascevich Jan 2, 2026
9837f5e
lower swift-log version again
kaascevich Jan 2, 2026
5d28af8
Merge branch 'stackotter:main' into swift-log
kaascevich Jan 2, 2026
dc3c131
add missing `return` to UIKitBackend init
kaascevich Jan 2, 2026
cf457ff
please just work this time
kaascevich Jan 2, 2026
7424008
commit Package.resolved properly
kaascevich Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ let package = Package(
"HotReloadingMacrosPlugin",
.product(name: "ImageFormats", package: "swift-image-formats"),
.product(name: "Logging", package: "swift-log"),
.product(name: "InMemoryLogging", package: "swift-log")
],
exclude: [
"Builders/ViewBuilder.swift.gyb",
Expand Down
103 changes: 43 additions & 60 deletions Sources/SwiftCrossUI/App.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import Foundation
import InMemoryLogging
import Logging

/// Backing storage for `logger`.
Expand Down Expand Up @@ -67,18 +66,24 @@ private var swiftBundlerAppMetadata: AppMetadata?

/// An error encountered when parsing Swift Bundler metadata.
private enum SwiftBundlerMetadataError: LocalizedError {
case noExecutableURL
case failedToReadExecutable
case jsonNotDictionary(String)
case missingAppIdentifier
case missingAppVersion

var errorDescription: String? {
switch self {
case .noExecutableURL:
"no executable URL"
case .failedToReadExecutable:
"executable failed to read itself (to extract metadata)"
case .jsonNotDictionary:
"Root metadata JSON value wasn't an object"
"root metadata JSON value wasn't an object"
case .missingAppIdentifier:
"Missing 'appIdentifier' (of type String)"
"missing 'appIdentifier' (of type String)"
case .missingAppVersion:
"Missing 'appVersion' (of type String)"
"missing 'appVersion' (of type String)"
}
}
}
Expand Down Expand Up @@ -119,48 +124,33 @@ extension App {
}

private static func extractMetadataAndInitializeLogging() {
// set up a temporary logger for `extractSwiftBundlerMetadata()` -- we
// won't initialize the actual logger until after that returns, so that
// users can use the app metadata in a custom logger
let temporaryLogHandler = InMemoryLogHandler()
_logger = Logger(
label: "SwiftCrossUI",
factory: { _ in temporaryLogHandler }
)

swiftBundlerAppMetadata = extractSwiftBundlerMetadata()
// extract metadata _before_ initializing the logger, so users can use
// said metadata when declaring a custom logger
let result = Result {
swiftBundlerAppMetadata = try extractSwiftBundlerMetadata()
}

// now initialize the real thing...
_logger = Logger(
label: "SwiftCrossUI",
factory: logHandler(label:metadataProvider:)
)

// ...and print out any log entries
for entry in temporaryLogHandler.entries {
logger.log(
level: entry.level,
entry.message,
metadata: entry.metadata
// check for an error once the logger is ready
if case .failure(let error) = result {
logger.error(
"failed to extract swift-bundler metadata",
metadata: ["error": "\(error)"]
)
}
}

private static func extractSwiftBundlerMetadata() -> AppMetadata? {
// NB: The logger isn't yet set up when this is called (it's initialized
// after this so that custom log handlers can refer to the app
// metadata). Since errors in this function are important to be able to
// debug, we store logged messages in `extractSwiftBundlerMetadataLogs`
// and dump them all as soon as the logger is ready.

private static func extractSwiftBundlerMetadata() throws -> AppMetadata? {
guard let executable = Bundle.main.executableURL else {
logger.warning("no executable url")
return nil
throw SwiftBundlerMetadataError.noExecutableURL
}

guard let data = try? Data(contentsOf: executable) else {
logger.warning("executable failed to read itself (to extract metadata)")
return nil
throw SwiftBundlerMetadataError.failedToReadExecutable
}

// Check if executable has Swift Bundler metadata magic bytes.
Expand All @@ -174,35 +164,28 @@ extension App {
let jsonStart = lengthStart - Int(jsonLength)
let jsonData = Data(bytes[jsonStart..<lengthStart])

do {
// Manually parsed due to the `additionalMetadata` field (which would
// require a lot of boilerplate code to parse with Codable).
let jsonValue = try JSONSerialization.jsonObject(with: jsonData)
guard let json = jsonValue as? [String: Any] else {
throw SwiftBundlerMetadataError.jsonNotDictionary(String(describing: jsonValue))
}
guard let identifier = json["appIdentifier"] as? String else {
throw SwiftBundlerMetadataError.missingAppIdentifier
}
guard let version = json["appVersion"] as? String else {
throw SwiftBundlerMetadataError.missingAppVersion
}
let additionalMetadata =
json["additionalMetadata"].map { value in
value as? [String: Any] ?? [:]
} ?? [:]
return AppMetadata(
identifier: identifier,
version: version,
additionalMetadata: additionalMetadata
)
} catch {
logger.warning(
"swift-bundler metadata present but couldn't be parsed",
metadata: ["error": "\(error)"]
)
return nil
// Manually parsed due to the `additionalMetadata` field (which would
// require a lot of boilerplate code to parse with Codable).
let jsonValue = try JSONSerialization.jsonObject(with: jsonData)
guard let json = jsonValue as? [String: Any] else {
throw SwiftBundlerMetadataError.jsonNotDictionary(String(describing: jsonValue))
}
guard let identifier = json["appIdentifier"] as? String else {
throw SwiftBundlerMetadataError.missingAppIdentifier
}
guard let version = json["appVersion"] as? String else {
throw SwiftBundlerMetadataError.missingAppVersion
}
let additionalMetadata =
json["additionalMetadata"].map { value in
value as? [String: Any] ?? [:]
} ?? [:]

return AppMetadata(
identifier: identifier,
version: version,
additionalMetadata: additionalMetadata
)
}

private static func parseBigEndianUInt64(
Expand Down