Turn on strict memory safety for the macOS app

This commit is contained in:
Ian Gulliver
2026-10-17 09:57:28 -07:00
parent 3541abb1b1
commit 7452bd5936
9 changed files with 33 additions and 32 deletions
-2
View File
@@ -16,8 +16,6 @@
# M8 — macOS client # M8 — macOS client
- macOS client strict memory safety
Turn on `.strictMemorySafety()` for the `Byre` target in `mac/Package.swift` and resolve what it flags (`@unchecked Sendable` on `Re`, `MainActor.assumeIsolated` in the key and click monitors, and anything else), so `swift build` is warning-free with it on.
- macOS client signing and SwiftData with iCloud sync - macOS client signing and SwiftData with iCloud sync
Sign `byre.app` with the paid developer team (provisioning profile, iCloud and CloudKit entitlements) in `mac/bundle.sh`, and move the app's persisted state (the `byre.server` origin, drafts, last visits) from `UserDefaults` to SwiftData with CloudKit sync, so the server address follows the user to every Mac. Sign `byre.app` with the paid developer team (provisioning profile, iCloud and CloudKit entitlements) in `mac/bundle.sh`, and move the app's persisted state (the `byre.server` origin, drafts, last visits) from `UserDefaults` to SwiftData with CloudKit sync, so the server address follows the user to every Mac.
- macOS client parity review - macOS client parity review
+2 -1
View File
@@ -25,7 +25,8 @@ let package = Package(
.product(name: "SwiftTerm", package: "SwiftTerm"), .product(name: "SwiftTerm", package: "SwiftTerm"),
.product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"),
.product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"), .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession"),
] ],
swiftSettings: [.strictMemorySafety()]
), ),
] ]
) )
+7 -2
View File
@@ -277,8 +277,13 @@ struct DiagramLayout {
let size: CGSize let size: CGSize
} }
nonisolated(unsafe) private let labelFont = Fonts.ct(13) private var labelFont: CTFont {
nonisolated(unsafe) private let edgeFont = Fonts.ct(12) Fonts.ns(13) as CTFont
}
private var edgeFont: CTFont {
Fonts.ns(12) as CTFont
}
func textSize(_ text: String, _ font: CTFont) -> CGSize { func textSize(_ text: String, _ font: CTFont) -> CGSize {
let lines = text.components(separatedBy: "\n") let lines = text.components(separatedBy: "\n")
+2 -2
View File
@@ -26,9 +26,9 @@ private func bytes(_ n: Int64) -> String {
return "\(n) B" return "\(n) B"
} }
if n < 1024 * 1024 { if n < 1024 * 1024 {
return String(format: "%.1f KiB", Double(n) / 1024) return "\((Double(n) / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) KiB"
} }
return String(format: "%.1f MiB", Double(n) / 1024 / 1024) return "\((Double(n) / 1024 / 1024).formatted(.number.precision(.fractionLength(1)).grouping(.never))) MiB"
} }
private func shown(_ c: FileContent) -> Int64 { private func shown(_ c: FileContent) -> Int64 {
+9 -12
View File
@@ -1,4 +1,5 @@
import CoreText import CoreText
import Synchronization
import SwiftUI import SwiftUI
enum Fonts { enum Fonts {
@@ -14,7 +15,7 @@ enum Fonts {
}() }()
private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor { private static func file(_ subset: String, _ italic: Bool, _ weight: CGFloat) -> CTFontDescriptor {
let d = descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]! let d = unsafe descriptors["atkinson-hyperlegible-mono-\(subset)-wght-\(italic ? "italic" : "normal").woff2"]!
return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight) return CTFontDescriptorCreateCopyWithVariation(d, 0x7767_6874 as CFNumber, weight)
} }
@@ -24,29 +25,25 @@ enum Fonts {
return CTFontCreateWithFontDescriptor(d, size, nil) return CTFontCreateWithFontDescriptor(d, size, nil)
} }
nonisolated(unsafe) private static var fonts: [String: (font: Font, ns: NSFont)] = [:] private static let fonts = Mutex<[String: CTFont]>([:])
private static let lock = NSLock()
private static func cached(_ size: CGFloat, _ weight: CGFloat, _ italic: Bool) -> (font: Font, ns: NSFont) { private static func cached(_ size: CGFloat, _ weight: CGFloat, _ italic: Bool) -> CTFont {
let key = "\(size) \(weight) \(italic)" let key = "\(size) \(weight) \(italic)"
lock.lock() return fonts.withLock { fonts in
defer {
lock.unlock()
}
if let f = fonts[key] { if let f = fonts[key] {
return f return f
} }
let c = ct(size, weight: weight, italic: italic) let f = ct(size, weight: weight, italic: italic)
let f = (Font(c), c as NSFont)
fonts[key] = f fonts[key] = f
return f return f
} }
}
static func font(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> Font { static func font(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> Font {
cached(size, weight, italic).font Font(cached(size, weight, italic))
} }
static func ns(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> NSFont { static func ns(_ size: CGFloat, weight: CGFloat = 400, italic: Bool = false) -> NSFont {
cached(size, weight, italic).ns cached(size, weight, italic) as NSFont
} }
} }
+6 -6
View File
@@ -112,7 +112,7 @@ struct InlineRenderer {
private func run(_ s: String, _ style: Style) -> AttributedString { private func run(_ s: String, _ style: Style) -> AttributedString {
var a = AttributedString(s) var a = AttributedString(s)
a.font = Fonts.font(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic) a.font = Fonts.font(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic)
a.appKit.font = Fonts.ns(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic) a.mergeAttributes(try! AttributeContainer([.font: Fonts.ns(style.small ? 11 : size, weight: style.bold ? 700 : 400, italic: style.italic)], including: \.appKit))
if let link = style.link { if let link = style.link {
a.link = link a.link = link
} }
@@ -364,7 +364,7 @@ private struct FlowBuilder {
s.append(NSAttributedString(string: "\n", attributes: [.font: Fonts.ns(ctx.size)])) s.append(NSAttributedString(string: "\n", attributes: [.font: Fonts.ns(ctx.size)]))
let all = NSRange(location: 0, length: s.length) let all = NSRange(location: 0, length: s.length)
let color = NSColor(ctx.color) let color = NSColor(ctx.color)
s.enumerateAttribute(.foregroundColor, in: all) { value, range, _ in unsafe s.enumerateAttribute(.foregroundColor, in: all) { value, range, _ in
if value == nil { if value == nil {
s.addAttribute(.foregroundColor, value: color, range: range) s.addAttribute(.foregroundColor, value: color, range: range)
} }
@@ -405,8 +405,8 @@ private struct FlowText: NSViewRepresentable {
v.isVerticallyResizable = false v.isVerticallyResizable = false
v.isHorizontallyResizable = false v.isHorizontallyResizable = false
v.textContainerInset = .zero v.textContainerInset = .zero
v.textContainer!.lineFragmentPadding = 0 unsafe v.textContainer!.lineFragmentPadding = 0
v.textContainer!.widthTracksTextView = true unsafe v.textContainer!.widthTracksTextView = true
v.linkTextAttributes = [.cursor: NSCursor.pointingHand] v.linkTextAttributes = [.cursor: NSCursor.pointingHand]
v.delegate = context.coordinator v.delegate = context.coordinator
return v return v
@@ -414,11 +414,11 @@ private struct FlowText: NSViewRepresentable {
func updateNSView(_ v: FlowTextView, context: Context) { func updateNSView(_ v: FlowTextView, context: Context) {
context.coordinator.openURL = context.environment.openURL context.coordinator.openURL = context.environment.openURL
if v.textStorage!.isEqual(to: text) { if unsafe v.textStorage!.isEqual(to: text) {
return return
} }
let selected = v.selectedRanges let selected = v.selectedRanges
v.textStorage!.setAttributedString(text) unsafe v.textStorage!.setAttributedString(text)
if selected.allSatisfy({ NSMaxRange($0.rangeValue) <= text.length }) { if selected.allSatisfy({ NSMaxRange($0.rangeValue) <= text.length }) {
v.selectedRanges = selected v.selectedRanges = selected
} }
+2 -2
View File
@@ -166,7 +166,7 @@ struct WindowReader: NSViewRepresentable {
func makeNSView(context: Context) -> NSView { func makeNSView(context: Context) -> NSView {
let view = NSView() let view = NSView()
DispatchQueue.main.async { DispatchQueue.main.async {
if let w = view.window { if let w = unsafe view.window {
found(w) found(w)
} }
} }
@@ -175,7 +175,7 @@ struct WindowReader: NSViewRepresentable {
func updateNSView(_ nsView: NSView, context: Context) { func updateNSView(_ nsView: NSView, context: Context) {
DispatchQueue.main.async { DispatchQueue.main.async {
if let w = nsView.window { if let w = unsafe nsView.window {
found(w) found(w)
} }
} }
+1 -1
View File
@@ -30,7 +30,7 @@ private func bytes(_ n: Int64) -> String {
v /= 1024 v /= 1024
i += 1 i += 1
} }
return String(format: i == 0 ? "%.0f %@" : "%.1f %@", v, units[i]) return "\(v.formatted(.number.precision(.fractionLength(i == 0 ? 0 : 1)).grouping(.never))) \(units[i])"
} }
private func resetText(_ unixSeconds: Double) -> String { private func resetText(_ unixSeconds: Double) -> String {
+1 -1
View File
@@ -82,7 +82,7 @@ struct TerminalHost: NSViewRepresentable {
context.coordinator.view = t context.coordinator.view = t
DispatchQueue.main.async { DispatchQueue.main.async {
colour(t) colour(t)
t.window?.makeFirstResponder(t) unsafe t.window?.makeFirstResponder(t)
context.coordinator.connect() context.coordinator.connect()
} }
return t return t