### Milestone: Remove IRCKit, migrate to swift-nio IRC, enable RoyalVNC via SwiftPM, and fix runtime embedding

- Replaced all IRCKit (0.16) usage with NozeIO `swift-nio-irc-client` (SwiftPM).
- Enabled IRC window with new client: connects and joins `#prole` (plain TCP for now; SSL checkbox displays a notice).
- Enabled RoyalVNC and removed all optional/flag-based handling. Integrated RoyalVNC via SwiftPM (`royalvnc` on `main`).
- Updated VNC UI to current RoyalVNC API: `VNCConnection` + `VNCCAFramebufferView` with a strong delegate reference.
- Simplified build to use SwiftPM for both IRC and RoyalVNC.
- Fixed runtime embedding and loader issues:
  - Copy SwiftPM-built `.dylib` products (e.g., `libRoyalVNCKit.dylib`) into `Contents/Frameworks` and codesign them.
  - Added `@executable_path/../Frameworks` to app rpaths with `install_name_tool`.
- Removed manual RoyalVNCKit `.xcframework` building/embedding and any IRCKit traces.

#### Key changes
- `prole-app/build.sh`:
  - Build app via SwiftPM; removed IRCKit and manual RoyalVNC build logic.
  - Embed SwiftPM `.dylib` outputs into `Contents/Frameworks` and set app rpath.
  - Cleaned usage text; dependencies now handled by SwiftPM.
- `prole-app/Package.swift`:
  - Add `swift-nio-irc-client` (NozeIO) dependency.
  - Add RoyalVNC via SwiftPM: `https://github.com/royalapplications/royalvnc` on `main`.
- `prole-app/Sources/IRCWindowController.swift`:
  - Migrate to NozeIO `IRC` package API; re-enable Connect; join `#prole`.
  - Import AppKit; provide convenience init for transcript view.
- `prole-app/Sources/WorkstationWindowController.swift`:
  - Import `RoyalVNCKit`; use `VNCConnection` + `VNCCAFramebufferView`.
  - Implement `VNCConnectionDelegate` and keep a strong reference to the delegate.
- `prole-app/debug.sh`:
  - Removed IRCKit logs section; kept RoyalVNC logs earlier; then removed manual RoyalVNC altogether.

This build now launches successfully (no dyld errors), opens the Workstation window (RoyalVNC), and the IRC window connects via the new client.

### Suggested follow-ups
- If TLS is required for IRC, add `NIOSSL` integration and wire SSL checkbox to TLS connection.
- Optionally remove leftover Vendor references if any local cache remains.
This commit is contained in:
chrisfu 2025-12-05 22:54:47 -08:00
parent e9d6e2af96
commit 64455fa886
9 changed files with 362 additions and 616 deletions

36
prole-app/Package.swift Normal file
View File

@ -0,0 +1,36 @@
// swift-tools-version: 5.9
import PackageDescription
let package = Package(
name: "ProleApp",
platforms: [
.macOS(.v12)
],
products: [
.executable(name: "Prole", targets: ["Prole"]) // App binary name
],
dependencies: [
// NozeIO SwiftNIO IRC Client
.package(url: "https://github.com/NozeIO/swift-nio-irc-client", branch: "main"),
// RoyalVNCKit via SwiftPM instead of local xcframework
.package(url: "https://github.com/royalapplications/royalvnc", branch: "main")
],
targets: [
.executableTarget(
name: "Prole",
dependencies: [
.product(name: "IRC", package: "swift-nio-irc-client"),
.product(name: "RoyalVNCKit", package: "royalvnc")
],
path: "Sources",
resources: [
// The build script separately copies resources into the app bundle; no SPM resources here.
],
linkerSettings: [
.linkedFramework("AppKit"),
.linkedFramework("Carbon"),
.linkedFramework("Network")
]
)
]
)

View File

@ -19,7 +19,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var hotKeyManager: HotKeyManager! private var hotKeyManager: HotKeyManager!
private var serviceChecker: ServiceChecker! private var serviceChecker: ServiceChecker!
private var statusItemController: StatusItemController! private var statusItemController: StatusItemController!
private var splashTipController: SplashTipWindowController? // Splash screen removed — keep no reference
private var appMenuToggleStatusBarItem: NSMenuItem? private var appMenuToggleStatusBarItem: NSMenuItem?
private var pfManager: PortForwardManager? private var pfManager: PortForwardManager?
@ -37,10 +37,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.setActivationPolicy(.regular) NSApp.setActivationPolicy(.regular)
dlog("Activation policy set to .regular (starting in App Window mode)") dlog("Activation policy set to .regular (starting in App Window mode)")
// Show startup tip splash for 5 seconds with animated GIF and counter // Splash removed: start directly
let splash = SplashTipWindowController()
self.splashTipController = splash
splash.show()
// ServiceChecker does the lightweight TCP checks on a background timer // ServiceChecker does the lightweight TCP checks on a background timer
serviceChecker = ServiceChecker() serviceChecker = ServiceChecker()
@ -114,15 +111,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
dlog("Starting ServiceChecker timer (interval: 30s)") dlog("Starting ServiceChecker timer (interval: 30s)")
serviceChecker.start(interval: 30) serviceChecker.start(interval: 30)
// Release the splash controller reference shortly after it is expected to auto-dismiss // No splash controller to release
DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) { [weak self] in
self?.splashTipController = nil
}
// Build application menu (shown when activation policy is .regular) // Build application menu (shown when activation policy is .regular)
setupApplicationMenu() setupApplicationMenu()
// Start in Application Window mode by default // Start in Application Window mode by default
// Ensure main window is positioned at the left and visible immediately
if let mainWin = mainWindowController.window { MainWindowController.positionWindowAtLeftEdge(mainWin) }
switchToAppWindowMode() switchToAppWindowMode()
// Ensure menu reflects current visibility // Ensure menu reflects current visibility
statusItemController.setState(statusBarVisible: statusItemController.isVisible, appWindowVisible: mainWindowController.isVisible) statusItemController.setState(statusBarVisible: statusItemController.isVisible, appWindowVisible: mainWindowController.isVisible)

View File

@ -69,15 +69,7 @@ final class Config {
var localHost: String { string("k3d.local.host", default: "localhost") } var localHost: String { string("k3d.local.host", default: "localhost") }
var localPort: Int { int("k3d.local.port", default: 6443) } var localPort: Int { int("k3d.local.port", default: 6443) }
// UI asset config // UI background config was removed along with the splash screen.
// Background image file name relative to the app bundle Resources. If a path is provided, only the last component is used.
var uiBackground: String {
let raw = string("ui.background", default: "img/proleLogoSepia.png")
if let lastSlash = raw.lastIndex(of: "/") {
return String(raw[raw.index(after: lastSlash)...])
}
return raw
}
// Dev port-forward supervision // Dev port-forward supervision
var pfEnabled: Bool { var pfEnabled: Bool {

View File

@ -1,7 +1,6 @@
import Foundation
import AppKit import AppKit
#if canImport(IRCKit) import IRC
import IRCKit
#endif
// A secondary window for IRC chat: short vertically, long horizontally. // A secondary window for IRC chat: short vertically, long horizontally.
// Title: "Prole IRC". Positioned below the main Prole Status window. // Title: "Prole IRC". Positioned below the main Prole Status window.
@ -39,13 +38,13 @@ final class IRCWindowController: NSWindowController {
private let kServerKey = "irc.server" private let kServerKey = "irc.server"
private let kSSLKey = "irc.ssl" private let kSSLKey = "irc.ssl"
#if canImport(IRCKit) private var ircClient: IRCClient?
private var client: IRCClient? private var joinedDefaultChannel = false
#endif
init(initialServer: String? = nil, initialSSL: Bool? = nil) { init(initialServer: String? = nil, initialSSL: Bool? = nil) {
let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable] let style: NSWindow.StyleMask = [.titled, .closable, .miniaturizable, .resizable]
let initialRect = NSRect(x: 0, y: 0, width: 1040, height: 300) // Start with the same width as the Main (Prole Status) window (720)
let initialRect = NSRect(x: 0, y: 0, width: 720, height: 300)
let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false) let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false)
super.init(window: window) super.init(window: window)
@ -54,6 +53,7 @@ final class IRCWindowController: NSWindowController {
window.level = .normal window.level = .normal
window.collectionBehavior = [.canJoinAllSpaces] window.collectionBehavior = [.canJoinAllSpaces]
window.appearance = NSAppearance(named: .aqua) window.appearance = NSAppearance(named: .aqua)
window.minSize = NSSize(width: 480, height: 200)
// Restore persisted values // Restore persisted values
let savedServer = initialServer ?? defaults.string(forKey: kServerKey) ?? "localhost" let savedServer = initialServer ?? defaults.string(forKey: kServerKey) ?? "localhost"
@ -74,10 +74,15 @@ final class IRCWindowController: NSWindowController {
topBar.translatesAutoresizingMaskIntoConstraints = false topBar.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(topBar) content.addSubview(topBar)
// Fix server text field width approximately to 32 characters // Make the server field flexible (no fixed minimum). It should stretch to fill available space.
let charWidth: CGFloat = 8.0 // approx for monospaced at system size serverField.setContentHuggingPriority(.defaultLow, for: .horizontal)
let targetWidth: CGFloat = charWidth * 32.0 serverField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
serverField.widthAnchor.constraint(greaterThanOrEqualToConstant: targetWidth).isActive = true connectButton.setContentHuggingPriority(.required, for: .horizontal)
connectButton.setContentCompressionResistancePriority(.required, for: .horizontal)
sslCheckbox.setContentHuggingPriority(.required, for: .horizontal)
sslCheckbox.setContentCompressionResistancePriority(.required, for: .horizontal)
portSuffixLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
portSuffixLabel.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal)
topBar.addArrangedSubview(NSTextField(labelWithString: "Server:")) topBar.addArrangedSubview(NSTextField(labelWithString: "Server:"))
topBar.addArrangedSubview(serverField) topBar.addArrangedSubview(serverField)
@ -99,7 +104,8 @@ final class IRCWindowController: NSWindowController {
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
topBar.topAnchor.constraint(equalTo: topAnchorRef, constant: 8), topBar.topAnchor.constraint(equalTo: topAnchorRef, constant: 8),
topBar.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8), topBar.leadingAnchor.constraint(equalTo: leadingAnchorRef, constant: 8),
topBar.trailingAnchor.constraint(lessThanOrEqualTo: trailingAnchorRef, constant: -8), // Make the top bar span full width so its children (server field) can stretch
topBar.trailingAnchor.constraint(equalTo: trailingAnchorRef, constant: -8),
transcriptView.topAnchor.constraint(equalTo: topBar.bottomAnchor, constant: 8), transcriptView.topAnchor.constraint(equalTo: topBar.bottomAnchor, constant: 8),
transcriptView.leadingAnchor.constraint(equalTo: leadingAnchorRef), transcriptView.leadingAnchor.constraint(equalTo: leadingAnchorRef),
@ -111,11 +117,8 @@ final class IRCWindowController: NSWindowController {
sslCheckbox.target = self; sslCheckbox.action = #selector(didToggleSSL) sslCheckbox.target = self; sslCheckbox.action = #selector(didToggleSSL)
connectButton.target = self; connectButton.action = #selector(didTapConnect) connectButton.target = self; connectButton.action = #selector(didTapConnect)
#if !canImport(IRCKit) connectButton.isEnabled = true
connectButton.isEnabled = false connectButton.toolTip = "Connect to #prole"
connectButton.toolTip = "IRCKit not linked — cannot connect"
transcriptView.appendLine("IRCKit not linked — chat disabled.")
#endif
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@ -132,6 +135,8 @@ final class IRCWindowController: NSWindowController {
guard let this = window else { return } guard let this = window else { return }
let refFrame = refWindow.frame let refFrame = refWindow.frame
var newFrame = this.frame var newFrame = this.frame
// Match width to the reference (Prole Status) window
newFrame.size.width = refFrame.size.width
newFrame.origin.x = refFrame.origin.x newFrame.origin.x = refFrame.origin.x
newFrame.origin.y = refFrame.origin.y - newFrame.height - gap newFrame.origin.y = refFrame.origin.y - newFrame.height - gap
this.setFrame(newFrame, display: true, animate: false) this.setFrame(newFrame, display: true, animate: false)
@ -177,56 +182,88 @@ final class IRCWindowController: NSWindowController {
} }
private func connectTo(host: String, port: Int, ssl: Bool) { private func connectTo(host: String, port: Int, ssl: Bool) {
transcriptView.appendLine("Connecting to \(host):\(port) \(ssl ? "(SSL)" : "")…") // Note: TLS/SSL is not yet supported by the swift-nio-irc-client package here.
#if canImport(IRCKit) if ssl {
// Disconnect if already connected transcriptView.appendLine("Note: SSL/TLS not implemented in current IRC client; attempting plain connection…")
client?.disconnect(message: "reconnect") }
// IRCKit API surface (approximate based on typical IRC libs). Adjust if needed.
let nick = nickname() let nick = nickname()
let user = nick let options = IRCClientOptions(
let realName = "Prole" port: port,
host: host,
let configuration = IRCConfiguration( password: nil,
hostname: host, nickname: IRCNickName(nick)!,
port: UInt16(port), userInfo: IRCUserInfo(username: nick, hostname: host, servername: host, realname: "Prole")
secure: ssl,
nickname: nick,
username: user,
realname: realName
) )
let client = IRCClient(options: options)
self.ircClient = client
self.joinedDefaultChannel = false
let client = IRCClient(configuration: configuration) // Delegate callbacks
self.client = client class Delegate: IRCClientDelegate {
weak var owner: IRCWindowController?
init(owner: IRCWindowController) { self.owner = owner }
client.onConnect = { [weak self] in func client(_ client: IRCClient, registered nick: IRCNickName, with userInfo: IRCUserInfo) {
self?.transcriptView.appendLine("Connected. Joining #prole …") owner?.transcriptView.appendLine("Registered as \(nick.stringValue)")
client.join(channel: "#prole") // Join default channel
} if owner?.joinedDefaultChannel == false {
client.send(.otherCommand("JOIN", ["#prole"]))
client.onDisconnect = { [weak self] reason, _ in owner?.joinedDefaultChannel = true
self?.transcriptView.appendLine("Disconnected: \(reason ?? "unknown")") }
} }
func clientFailedToRegister(_ client: IRCClient) {
client.onMessage = { [weak self] message in owner?.transcriptView.appendLine("Failed to register with server")
guard let self = self else { return } }
let nick = message.sender?.nickname ?? "?" func client(_ client: IRCClient, received message: IRCMessage) {
let text = message.message // Render a few common messages
self.transcriptView.appendLine("<\(nick)> \(text)") switch message.command {
} case .PRIVMSG(let target, let text):
// The upstream message model may not expose a prefix property consistently across versions.
client.onNotice = { [weak self] notice in // Fallback to unknown sender for now.
self?.transcriptView.appendLine("-notice- \(notice.message)") let from = "?"
} if case .channel(let ch) = target.first {
owner?.transcriptView.appendLine("[\(ch)] <\(from)> \(text)")
client.onError = { [weak self] err in } else {
self?.transcriptView.appendLine("Error: \(err.localizedDescription)") owner?.transcriptView.appendLine("<\(from)> \(text)")
}
case .NOTICE(_, let text):
owner?.transcriptView.appendLine("-notice- \(text)")
default:
break
}
}
func client(_ client: IRCClient, messageOfTheDay: String) {
owner?.transcriptView.appendLine("-motd- \(messageOfTheDay)")
}
func client(_ client: IRCClient, notice message: String, for recipients: [IRCMessageRecipient]) {
owner?.transcriptView.appendLine("-notice- \(message)")
}
func client(_ client: IRCClient, message: String, from user: IRCUserID, for recipients: [IRCMessageRecipient]) {
let who = user.nick.stringValue
owner?.transcriptView.appendLine("<\(who)> \(message)")
}
func client(_ client: IRCClient, changedUserModeTo mode: IRCUserMode) {}
func client(_ client: IRCClient, changedNickTo nick: IRCNickName) {
owner?.transcriptView.appendLine("You are now known as \(nick.stringValue)")
}
func client(_ client: IRCClient, user: IRCUserID, joined: [IRCChannelName]) {
let who = user.nick.stringValue
let channels = joined.map { $0.stringValue }.joined(separator: ", ")
owner?.transcriptView.appendLine("-- \(who) joined \(channels)")
}
func client(_ client: IRCClient, user: IRCUserID, left: [IRCChannelName], with: String?) {
let who = user.nick.stringValue
let channels = left.map { $0.stringValue }.joined(separator: ", ")
owner?.transcriptView.appendLine("-- \(who) left \(channels)")
}
func client(_ client: IRCClient, changeTopic: String, of channel: IRCChannelName) {
owner?.transcriptView.appendLine("-- topic for \(channel.stringValue): \(changeTopic)")
}
} }
client.delegate = Delegate(owner: self)
transcriptView.appendLine("Connecting to \(host):\(port)…")
client.connect() client.connect()
#else
transcriptView.appendLine("Cannot connect: IRCKit not linked")
#endif
} }
} }
@ -261,6 +298,10 @@ final class IRCTranscriptView: NSView {
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
convenience init() {
self.init(frame: .zero)
}
func appendLine(_ line: String) { func appendLine(_ line: String) {
let ts = IRCTranscriptView.timestamp() let ts = IRCTranscriptView.timestamp()
let s = "[\(ts)] \(line)\n" let s = "[\(ts)] \(line)\n"

View File

@ -34,8 +34,7 @@ final class MainWindowController: NSWindowController {
super.init(window: window) super.init(window: window)
window.isReleasedWhenClosed = false window.isReleasedWhenClosed = false
window.title = "ProleStatus" window.title = "Prole Status — ⌘⌥⇧P to toggle"
window.center()
window.level = .normal window.level = .normal
window.collectionBehavior = [.canJoinAllSpaces] window.collectionBehavior = [.canJoinAllSpaces]
window.appearance = NSAppearance(named: .aqua) window.appearance = NSAppearance(named: .aqua)
@ -125,6 +124,7 @@ final class MainWindowController: NSWindowController {
func show() { func show() {
guard let window = window else { return } guard let window = window else { return }
MainWindowController.positionWindowAtLeftEdge(window)
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: false) NSApp.activate(ignoringOtherApps: false)
} }
@ -135,6 +135,20 @@ final class MainWindowController: NSWindowController {
var isVisible: Bool { window?.isVisible ?? false } var isVisible: Bool { window?.isVisible ?? false }
// MARK: - Positioning helpers
static func positionWindowAtLeftEdge(_ window: NSWindow, margin: CGFloat = 12) {
guard let screen = window.screen ?? NSScreen.main else { return }
let vis = screen.visibleFrame
var frame = window.frame
// Place near left edge and below the menu bar, keep current size
frame.origin.x = vis.origin.x + margin
// Align top to visible frame top with small margin
frame.origin.y = vis.maxY - frame.height - margin
// Ensure not offscreen vertically
if frame.origin.y < vis.origin.y + margin { frame.origin.y = vis.origin.y + margin }
window.setFrame(frame, display: true, animate: false)
}
// MARK: - Actions // MARK: - Actions
@objc private func didTapRefresh() { actions.onRefresh() } @objc private func didTapRefresh() { actions.onRefresh() }
@objc private func didTapMinimize() { actions.onMinimizeToStatusBar() } @objc private func didTapMinimize() { actions.onMinimizeToStatusBar() }

View File

@ -1,145 +0,0 @@
import AppKit
final class SplashTipWindowController: NSWindowController {
private let imageView = NSImageView()
private let tipLabel = NSTextField(labelWithString: "Tip: Press Cmd+Opt+Shift+P to change modes")
private let counterLabel = NSTextField(labelWithString: "0.0")
private var timer: DispatchSourceTimer?
private var startTime: DispatchTime?
private let displayDuration: TimeInterval = 5.0
init() {
let style: NSWindow.StyleMask = [.borderless]
let initialRect = NSRect(x: 0, y: 0, width: 520, height: 360)
let window = NSWindow(contentRect: initialRect, styleMask: style, backing: .buffered, defer: false)
super.init(window: window)
window.isReleasedWhenClosed = false
// Light theme background
window.backgroundColor = NSColor(calibratedWhite: 1.0, alpha: 0.96)
window.isOpaque = false
window.hasShadow = true
window.level = .floating
window.collectionBehavior = [.canJoinAllSpaces]
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
window.appearance = NSAppearance(named: .aqua)
let content = NSView()
content.wantsLayer = true
content.translatesAutoresizingMaskIntoConstraints = false
window.contentView = content
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.imageScaling = .scaleProportionallyUpOrDown
imageView.animates = true
tipLabel.translatesAutoresizingMaskIntoConstraints = false
tipLabel.font = NSFont.systemFont(ofSize: 14, weight: .semibold)
tipLabel.textColor = .labelColor
tipLabel.alignment = .center
tipLabel.lineBreakMode = .byWordWrapping
tipLabel.maximumNumberOfLines = 2
counterLabel.translatesAutoresizingMaskIntoConstraints = false
counterLabel.font = NSFont.monospacedSystemFont(ofSize: 10, weight: .regular)
counterLabel.textColor = NSColor(white: 0.1, alpha: 0.85)
counterLabel.alignment = .left
content.addSubview(imageView)
content.addSubview(tipLabel)
content.addSubview(counterLabel)
NSLayoutConstraint.activate([
content.widthAnchor.constraint(equalToConstant: initialRect.width),
content.heightAnchor.constraint(equalToConstant: initialRect.height),
imageView.topAnchor.constraint(equalTo: content.topAnchor, constant: 20),
imageView.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
imageView.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
imageView.heightAnchor.constraint(equalToConstant: 260),
tipLabel.topAnchor.constraint(equalTo: imageView.bottomAnchor, constant: 12),
tipLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 20),
tipLabel.trailingAnchor.constraint(equalTo: content.trailingAnchor, constant: -20),
counterLabel.leadingAnchor.constraint(equalTo: content.leadingAnchor, constant: 6),
counterLabel.bottomAnchor.constraint(equalTo: content.bottomAnchor, constant: -4)
])
// Load background/splash image from config (bundled in Resources) if present
let bgName = Config.shared.uiBackground
let candidates: [(String, String?)] = [
(bgName, nil),
("prole-type", "gif")
]
var loaded = false
for (res, ext) in candidates {
if let url = Bundle.main.url(forResource: res, withExtension: ext),
let img = NSImage(contentsOf: url) {
imageView.image = img
loaded = true
break
}
}
if !loaded {
// Fallback: show placeholder text if the resource is missing
let placeholder = NSTextField(labelWithString: "Splash background not found")
placeholder.textColor = .secondaryLabelColor
placeholder.alignment = .center
placeholder.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(placeholder)
NSLayoutConstraint.activate([
placeholder.centerXAnchor.constraint(equalTo: imageView.centerXAnchor),
placeholder.centerYAnchor.constraint(equalTo: imageView.centerYAnchor)
])
}
// Click anywhere to dismiss early
let clickRecognizer = NSClickGestureRecognizer(target: self, action: #selector(dismissSplash))
content.addGestureRecognizer(clickRecognizer)
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
func show() {
guard let screen = NSScreen.main, let window = window else { return }
let frame = window.frame
let x = screen.visibleFrame.midX - frame.width / 2
let y = screen.visibleFrame.midY - frame.height / 2
window.setFrame(NSRect(x: x, y: y, width: frame.width, height: frame.height), display: true)
window.orderFrontRegardless()
NSApp.activate(ignoringOtherApps: false)
// Start counter timer
startTime = .now()
let timer = DispatchSource.makeTimerSource(queue: .main)
timer.schedule(deadline: .now(), repeating: .milliseconds(100))
timer.setEventHandler { [weak self] in
self?.tick()
}
timer.resume()
self.timer = timer
// Auto dismiss after duration
DispatchQueue.main.asyncAfter(deadline: .now() + displayDuration) { [weak self] in
self?.dismissSplash()
}
}
private func tick() {
guard let start = startTime else { return }
let elapsed = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds
let seconds = Double(elapsed) / 1_000_000_000.0
let whole = Int(seconds)
let tenths = Int((seconds - Double(whole)) * 10.0)
counterLabel.stringValue = String(format: "%d.%d", whole, tenths)
}
@objc private func dismissSplash() {
timer?.cancel()
timer = nil
window?.orderOut(nil)
close()
}
}

View File

@ -1,7 +1,5 @@
import AppKit import AppKit
#if canImport(RoyalVNCKit)
import RoyalVNCKit import RoyalVNCKit
#endif
// A secondary window that hosts the Prole Workstation (VNC viewer) // A secondary window that hosts the Prole Workstation (VNC viewer)
// Version 1: viewer-only (no interactive session wiring yet) // Version 1: viewer-only (no interactive session wiring yet)
@ -62,31 +60,18 @@ final class WorkstationWindowController: NSWindowController {
} }
} }
// Host view that embeds RoyalVNCKit viewer when available; otherwise shows a placeholder. // Host view that embeds RoyalVNCKit viewer.
final class VNCViewerHostView: NSView { final class VNCViewerHostView: NSView {
private let placeholder: NSTextField = { private var framebufferView: VNCCAFramebufferView?
let tf = NSTextField(labelWithString: "VNC viewer unavailable — RoyalVNCKit not linked") private var connection: VNCConnection?
tf.alignment = .center // Keep a strong reference to the delegate; VNCConnection holds it weakly
tf.textColor = .secondaryLabelColor private var connectionDelegateStrongRef: VNCConnectionDelegate?
tf.font = NSFont.systemFont(ofSize: 13)
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
override init(frame frameRect: NSRect) { override init(frame frameRect: NSRect) {
super.init(frame: frameRect) super.init(frame: frameRect)
wantsLayer = true wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
// The actual framebuffer view will be created when the connection creates the framebuffer
#if canImport(RoyalVNCKit)
// The actual viewer will be created in connectDefault()
#else
addSubview(placeholder)
NSLayoutConstraint.activate([
placeholder.centerXAnchor.constraint(equalTo: centerXAnchor),
placeholder.centerYAnchor.constraint(equalTo: centerYAnchor)
])
#endif
} }
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
@ -96,23 +81,70 @@ final class VNCViewerHostView: NSView {
} }
func connect(hostname: String, port: Int) { func connect(hostname: String, port: Int) {
#if canImport(RoyalVNCKit) // Construct full settings according to current RoyalVNCKit API
// The concrete API follows the RoyalVNCKit USAGE; adjust if needed when linking. let settings = VNCConnection.Settings(
let settings = VNCConnection.Settings(hostname: hostname, port: UInt16(port)) isDebugLoggingEnabled: false,
let connection = VNCConnection(settings: settings) hostname: hostname,
let vncView = VNCView(frame: bounds) port: UInt16(port),
vncView.translatesAutoresizingMaskIntoConstraints = false isShared: true,
addSubview(vncView) isScalingEnabled: true,
NSLayoutConstraint.activate([ useDisplayLink: true,
vncView.topAnchor.constraint(equalTo: topAnchor), inputMode: .forwardKeyboardShortcutsIfNotInUseLocally,
vncView.leadingAnchor.constraint(equalTo: leadingAnchor), isClipboardRedirectionEnabled: true,
vncView.trailingAnchor.constraint(equalTo: trailingAnchor), colorDepth: .depth24Bit,
vncView.bottomAnchor.constraint(equalTo: bottomAnchor) frameEncodings: .default
]) )
vncView.connect(connection)
#else let conn = VNCConnection(settings: settings)
// Nothing to do; placeholder already shown self.connection = conn
_ = hostname; _ = port
#endif // Minimal delegate to attach framebuffer to a view
class Delegate: VNCConnectionDelegate {
weak var host: VNCViewerHostView?
init(host: VNCViewerHostView) { self.host = host }
func connection(_ connection: VNCConnection, stateDidChange connectionState: VNCConnection.ConnectionState) {
// No-op for now; could update UI
}
func connection(_ connection: VNCConnection, credentialFor authenticationType: VNCAuthenticationType, completion: @escaping (VNCCredential?) -> Void) {
// For now, no auth
completion(nil)
}
func connection(_ connection: VNCConnection, didCreateFramebuffer framebuffer: VNCFramebuffer) {
guard let host = host else { return }
DispatchQueue.main.async {
let fbView = VNCCAFramebufferView(frame: host.bounds, framebuffer: framebuffer, connection: connection)
fbView.translatesAutoresizingMaskIntoConstraints = false
host.subviews.forEach { $0.removeFromSuperview() }
host.addSubview(fbView)
NSLayoutConstraint.activate([
fbView.topAnchor.constraint(equalTo: host.topAnchor),
fbView.leadingAnchor.constraint(equalTo: host.leadingAnchor),
fbView.trailingAnchor.constraint(equalTo: host.trailingAnchor),
fbView.bottomAnchor.constraint(equalTo: host.bottomAnchor)
])
host.framebufferView = fbView
}
}
func connection(_ connection: VNCConnection, didResizeFramebuffer framebuffer: VNCFramebuffer) {
// VNCCAFramebufferView queries connection/framebuffer for size; nothing required here
}
func connection(_ connection: VNCConnection, didUpdateFramebuffer framebuffer: VNCFramebuffer, x: UInt16, y: UInt16, width: UInt16, height: UInt16) {
// View should handle drawing updates internally
}
func connection(_ connection: VNCConnection, didUpdateCursor cursor: VNCCursor) {
// Cursor updates handled by the framebuffer view
}
}
let delegate = Delegate(host: self)
self.connectionDelegateStrongRef = delegate
conn.delegate = delegate
conn.connect()
} }
} }

View File

@ -24,7 +24,6 @@ LOG_DIR="$BUILD_DIR/logs"
ARCH_CURRENT="$(uname -m)" # arm64 or x86_64 ARCH_CURRENT="$(uname -m)" # arm64 or x86_64
# Mandatory dependency sources (auto-fetched) # Mandatory dependency sources (auto-fetched)
URL_IRCKIT="https://github.com/FuelRats/IRCKit/archive/refs/tags/0.16.0.tar.gz"
URL_ROYALVNC="https://github.com/royalapplications/royalvnc/archive/refs/tags/1.0.1.tar.gz" URL_ROYALVNC="https://github.com/royalapplications/royalvnc/archive/refs/tags/1.0.1.tar.gz"
# Deps staging directories # Deps staging directories
@ -56,16 +55,11 @@ Examples:
./build.sh package ./build.sh package
Dependencies: Dependencies:
This script can download, build, link, and embed IRCKit (0.16.0) IRC and RoyalVNC are built via Swift Package Manager.
and RoyalVNCKit (1.0.1). These are optional and controlled by flags below.
Environment overrides (optional): Environment overrides (optional):
IRCKIT_XCFRAMEWORK If set, use this IRCKit.xcframework instead of building
ROYALVNCKIT_XCFRAMEWORK If set, use this RoyalVNCKit.xcframework instead of building ROYALVNCKIT_XCFRAMEWORK If set, use this RoyalVNCKit.xcframework instead of building
KEEP_DEPS=1 Keep the deps staging directory after the build (for debugging) KEEP_DEPS=1 Keep the deps staging directory after the build (for debugging)
PROLE_DISABLE_SSL=1 Build IRCKit with SSL/TLS disabled (compilation condition); may also update deps to Swift‑6‑compatible NIO
PROLE_ENABLE_IRCKit=0 Enable building/linking IRCKit (0/1). Default: 0 (disabled)
PROLE_ENABLE_RoyalVNCKit=0 Enable building/linking RoyalVNCKit (0/1). Default: 0 (disabled)
EOF EOF
} }
@ -223,29 +217,6 @@ PLIST
} }
copy_extra_resources() { copy_extra_resources() {
# Copy animated tip GIF into app resources if it exists in repo
local gif_src="$PARENT_DIR/www/images/prole-type.gif"
if [[ -f "$gif_src" ]]; then
mkdir -p "$RESOURCES_DIR"
cp "$gif_src" "$RESOURCES_DIR/prole-type.gif"
echo "[resources] Copied prole-type.gif into Resources"
else
echo "[resources] prole-type.gif not found at $gif_src (skipping)"
fi
# Copy configured background image (ui.background) into Resources, keeping basename
local ui_bg
ui_bg="$(prop_get ui.background || true)"
if [[ -n "$ui_bg" && -f "$PARENT_DIR/$ui_bg" ]]; then
mkdir -p "$RESOURCES_DIR"
local base
base="$(basename "$ui_bg")"
cp "$PARENT_DIR/$ui_bg" "$RESOURCES_DIR/$base"
echo "[resources] Copied background image ($base) into Resources"
else
echo "[resources] ui.background not set or file missing (skipping)"
fi
# Copy default properties file if present # Copy default properties file if present
local props_src="$ROOT_DIR/prole.properties" local props_src="$ROOT_DIR/prole.properties"
if [[ -f "$props_src" ]]; then if [[ -f "$props_src" ]]; then
@ -256,7 +227,7 @@ copy_extra_resources() {
fi fi
} }
# --- Dependency preparation (mandatory IRCKit & RoyalVNCKit) --- # --- Dependency preparation (RoyalVNCKit) ---
have_cmd() { command -v "$1" >/dev/null 2>&1; } have_cmd() { command -v "$1" >/dev/null 2>&1; }
@ -295,22 +266,19 @@ xc_archive_one() {
proj_opt=( -project "${scheme}.xcodeproj" ) proj_opt=( -project "${scheme}.xcodeproj" )
fi fi
local logfile="$LOG_DIR/xcode-archive-${scheme}-${arch}.log" local logfile="$LOG_DIR/xcode-archive-${scheme}-${arch}.log"
if [[ "$scheme" == "IRCKit" ]]; then logfile="$DEPS_LOGS_DIR/irckit-archive-${arch}.log"; fi
if [[ "$scheme" == "RoyalVNCKit" ]]; then logfile="$DEPS_LOGS_DIR/royalvnc-archive-${arch}.log"; fi if [[ "$scheme" == "RoyalVNCKit" ]]; then logfile="$DEPS_LOGS_DIR/royalvnc-archive-${arch}.log"; fi
pushd "$proj_dir" >/dev/null || return 1 pushd "$proj_dir" >/dev/null || return 1
# Per-scheme extra flags / xcconfig # Per-scheme extra flags / xcconfig
local extra_args=() local extra_args=()
if [[ "$scheme" == "IRCKit" && -n ${PROLE_DISABLE_SSL:-} ]]; then if [[ "$scheme" == "RoyalVNCKit" ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: building IRCKit with PROLE_DISABLE_SSL compilation condition" # Enable distribution interfaces so SPM can import the Swift module, but disable verification to avoid toolchain issues
# Use a temporary xcconfig to avoid shell/argument parsing issues on the command line local xcflags_file="$outdir/royalvnc-archive-overrides.xcconfig"
local xcflags_file="$outdir/irckit-prole-disable-ssl.xcconfig"
# IMPORTANT: Keep this file strictly to valid xcconfig key/value lines (no comments or extra text)
cat > "$xcflags_file" <<'XCCONFIG' cat > "$xcflags_file" <<'XCCONFIG'
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PROLE_DISABLE_SSL BUILD_LIBRARY_FOR_DISTRIBUTION = YES
OTHER_SWIFT_FLAGS = $(inherited) -DPROLE_DISABLE_SSL SWIFT_EMIT_MODULE_INTERFACE = YES
SWIFT_VERSION = 5.0 SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
SWIFT_STRICT_CONCURRENCY = minimal OTHER_SWIFT_FLAGS = $(inherited) -no-verify-emitted-module-interface
XCCONFIG XCCONFIG
extra_args+=( -xcconfig "$xcflags_file" ) extra_args+=( -xcconfig "$xcflags_file" )
fi fi
@ -331,134 +299,47 @@ XCCONFIG
popd >/dev/null popd >/dev/null
} }
build_irckit_xcframework() { # Build one framework w/ xcodebuild (not archive), outputting a .framework into outdir
# If external override exists, trust it xc_build_framework_one() {
if [[ -n ${IRCKIT_XCFRAMEWORK:-} && -d ${IRCKIT_XCFRAMEWORK} ]]; then local proj_dir="$1"; local scheme="$2"; local arch="$3"; local outdir="$4"; local product_name="$5"
echo "[deps] Using provided IRCKit.xcframework: ${IRCKIT_XCFRAMEWORK}" local build_dir="$outdir/${scheme}-macos-${arch}-build"
mkdir -p "$build_dir"
local logfile="$DEPS_LOGS_DIR/${scheme}-build-${arch}.log"
pushd "$proj_dir" >/dev/null || return 1
# Prepare xcconfig to disable interface emission/verification
local xcflags_file="$outdir/${scheme}-build-overrides.${arch}.xcconfig"
cat > "$xcflags_file" <<'XCCONFIG'
// Emit module interfaces suitable for distribution so the Swift module can be imported by SPM
BUILD_LIBRARY_FOR_DISTRIBUTION = YES
SWIFT_EMIT_MODULE_INTERFACE = YES
SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
// But do not verify emitted module interfaces to avoid toolchain-specific failures
OTHER_SWIFT_FLAGS = $(inherited) -no-verify-emitted-module-interface
XCCONFIG
# Important: write all xcodebuild output to the log to keep stdout clean (we echo only the path below)
xcodebuild build \
-scheme "$scheme" \
-destination 'generic/platform=macOS' \
-configuration Release \
-sdk macosx \
ARCHS="$arch" ONLY_ACTIVE_ARCH=NO \
SWIFT_VERSION=5.0 \
SWIFT_STRICT_CONCURRENCY=minimal \
MACOSX_DEPLOYMENT_TARGET="${MIN_MACOS}" \
CONFIGURATION_BUILD_DIR="$build_dir" \
-xcconfig "$xcflags_file" \
>"$logfile" 2>&1 || { popd >/dev/null; return 1; }
popd >/dev/null
# Accept both plain build dir and SwiftPM PackageFrameworks location
if [[ -d "$build_dir/${product_name}.framework" ]]; then
echo "$build_dir/${product_name}.framework"
return 0 return 0
fi fi
if [[ -d "$build_dir/PackageFrameworks/${product_name}.framework" ]]; then
local work="$DEPS_SRC_DIR/IRCKit-src" echo "$build_dir/PackageFrameworks/${product_name}.framework"
local tar="$DEPS_DIR/irckit.tar.gz" return 0
rm -rf "$work"; mkdir -p "$work"
fetch_tarball "$URL_IRCKIT" "$tar"
extract_tarball "$tar" "$work"
# Detect package or project root robustly
local src_root=""
# 1) If a Package.swift exists directly under work
if [[ -f "$work/Package.swift" ]]; then
src_root="$work"
fi fi
# 2) Else pick the first child directory that contains a Package.swift return 1
if [[ -z "$src_root" ]]; then
for d in "$work"/*; do
[[ -d "$d" ]] || continue
if [[ -f "$d/Package.swift" ]]; then src_root="$d"; break; fi
done
fi
# 3) Else fall back to the first child directory (common for GitHub tarballs)
if [[ -z "$src_root" ]]; then
for d in "$work"/*; do
[[ -d "$d" ]] || continue
src_root="$d"; break
done
fi
if [[ -z "$src_root" ]]; then
echo "[deps] error: IRCKit source not found after extract" >&2
exit 1
fi
echo "[deps] IRCKit source root: $src_root"
pushd "$src_root" >/dev/null
# Ensure SwiftPM uses Swift 5 toolchain mode (avoid Swift 6 defaults under Xcode 16.x)
if have_cmd swift; then
echo "[deps] Forcing SwiftPM tools-version to 5.9 for IRCKit"
log_exec "$DEPS_LOGS_DIR/irckit-spm-tools-version.log" swift package tools-version --set 5.9 || true
fi
# Under SSL-less builds, strip NIOSSL from the graph and pin swift-nio to a Swift‑5–compatible release
if [[ -n ${PROLE_DISABLE_SSL:-} && -f "Package.swift" ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: patching IRCKit Package.swift to remove NIOSSL and pin swift-nio to 2.40.0 (exact)"
cp -f "Package.swift" "$DEPS_LOGS_DIR/irckit-Package.swift.before" 2>/dev/null || true
# 1) Remove swift-nio-ssl package declaration lines entirely
perl -0777 -pe 's|\s*\.package\(\s*url:\s*"https://github\.com/apple/swift-nio-ssl"\s*,\s*[^\)]*\),?\n||g' -i "Package.swift" 2>/dev/null || true
# 2) Remove NIOSSL from any target dependency arrays (handles product and target name forms)
perl -0777 -pe 's|("|\[)NIOSSL(\]|"),?\s*||g; s|\[\s*,\s*\]|[]|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\s*\.product\(name:\s*"NIOSSL"[^\)]*\),?\n||g' -i "Package.swift" 2>/dev/null || true
# 3) Pin swift-nio to an exact, Swift‑5–compatible version to avoid Swift‑6‑only codepaths
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*from:\s*"[^"]+"\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*\.upToNextMajor\(from:\s*"[^"]+"\)\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-nio"\s*,\s*\.exact\([^\)]*\)\s*\)|.package(url: "https://github.com/apple/swift-nio", .exact("2.40.0"))|g' -i "Package.swift" 2>/dev/null || true
# 3b) Optionally pin swift-atomics to a Swift‑5–compatible release to help NIOConcurrencyHelpers
if ! grep -q "swift-atomics" Package.swift 2>/dev/null; then
echo "[deps] PROLE_DISABLE_SSL=1: adding swift-atomics .exact(\"1.0.2\") to Package.swift to stabilize NIOConcurrencyHelpers"
perl -0777 -pe 's|(dependencies\s*:\s*\[)|$1\n .package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2")),|s' -i "Package.swift" 2>/dev/null || true
else
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-atomics"\s*,\s*from:\s*"[^"]+"\s*\)|.package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2"))|g' -i "Package.swift" 2>/dev/null || true
perl -0777 -pe 's|\.package\(\s*url:\s*"https://github\.com/apple/swift-atomics"\s*,\s*\.upToNextMajor\(from:\s*"[^"]+"\)\s*\)|.package(url: "https://github.com/apple/swift-atomics", .exact("1.0.2"))|g' -i "Package.swift" 2>/dev/null || true
fi
# 4) Ensure the package explicitly targets Swift 5 language mode if not already present
if ! grep -q "swiftLanguageVersions" Package.swift 2>/dev/null; then
echo "[deps] PROLE_DISABLE_SSL=1: injecting swiftLanguageVersions = [.v5] into Package.swift"
# Try to insert after the targets array before the closing paren of Package initializer
perl -0777 -pe 's|(targets\s*:\s*\[[\s\S]*?\])\s*\)\s*$|$1,\n swiftLanguageVersions: [.v5]\n)\n|s' -i "Package.swift" 2>/dev/null || true
fi
cp -f "Package.swift" "$DEPS_LOGS_DIR/irckit-Package.swift.after" 2>/dev/null || true
fi
# If SSL is disabled, avoid updating to latest; just resolve with our pinned/stripped graph
if have_cmd swift; then
if [[ -n ${PROLE_DISABLE_SSL:-} ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: resolving IRCKit SwiftPM dependencies (no update)"
log_exec "$DEPS_LOGS_DIR/irckit-spm-resolve.log" swift package resolve || true
else
echo "[deps] Resolving IRCKit SwiftPM dependencies as pinned (no update)"
log_exec "$DEPS_LOGS_DIR/irckit-spm-resolve.log" swift package resolve || true
fi
# Best-effort diagnostics of the resolved graph
log_exec "$DEPS_LOGS_DIR/irckit-spm-show-deps.log" swift package show-dependencies || true
if [[ -f .swiftpm/resolved/Package.resolved ]]; then
cp -f .swiftpm/resolved/Package.resolved "$DEPS_LOGS_DIR/irckit-Package.resolved" || true
fi
fi
# Note: Avoid surgical source patches; rely on version pinning per user request
local build_tmp="$DEPS_OUT_DIR/IRCKit-build"
rm -rf "$build_tmp"; mkdir -p "$build_tmp"
local have_arm=0; local have_x86=0
# Determine which architectures to build for dependencies
local want_arches=(arm64 x86_64)
if [[ "${PROLE_BUILD_ARCH:-}" == "arm64" ]]; then
want_arches=(arm64)
elif [[ "${PROLE_BUILD_ARCH:-}" == "x86_64" ]]; then
want_arches=(x86_64)
fi
for dep_arch in "${want_arches[@]}"; do
if [[ "$dep_arch" == "arm64" ]]; then
if xc_archive_one "$PWD" IRCKit arm64 "$build_tmp"; then have_arm=1; fi
else
if xc_archive_one "$PWD" IRCKit x86_64 "$build_tmp"; then have_x86=1; fi
fi
done
local out_xc="$DEPS_OUT_DIR/IRCKit.xcframework"
rm -rf "$out_xc"
if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-arm64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-framework "$build_tmp/IRCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-arm64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
elif [[ $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/irckit-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/IRCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/IRCKit.framework" \
-output "$out_xc"
else
echo "[deps] error: failed to archive IRCKit for any macOS arch" >&2
exit 1
fi
export IRCKIT_XCFRAMEWORK="$out_xc"
echo "[deps] Built IRCKit.xcframework at $IRCKIT_XCFRAMEWORK"
popd >/dev/null
} }
build_royalvnc_xcframework() { build_royalvnc_xcframework() {
@ -481,6 +362,7 @@ build_royalvnc_xcframework() {
local build_tmp="$DEPS_OUT_DIR/RoyalVNC-build" local build_tmp="$DEPS_OUT_DIR/RoyalVNC-build"
rm -rf "$build_tmp"; mkdir -p "$build_tmp" rm -rf "$build_tmp"; mkdir -p "$build_tmp"
local have_arm=0; local have_x86=0 local have_arm=0; local have_x86=0
local fw_arm=""; local fw_x86=""
local want_arches=(arm64 x86_64) local want_arches=(arm64 x86_64)
if [[ "${PROLE_BUILD_ARCH:-}" == "arm64" ]]; then if [[ "${PROLE_BUILD_ARCH:-}" == "arm64" ]]; then
want_arches=(arm64) want_arches=(arm64)
@ -489,58 +371,55 @@ build_royalvnc_xcframework() {
fi fi
for dep_arch in "${want_arches[@]}"; do for dep_arch in "${want_arches[@]}"; do
if [[ "$dep_arch" == "arm64" ]]; then if [[ "$dep_arch" == "arm64" ]]; then
if xc_archive_one "$PWD" RoyalVNCKit arm64 "$build_tmp"; then have_arm=1; fi # Prefer archive to produce proper Swift module interfaces; fall back to build if archive fails
if xc_archive_one "$PWD" RoyalVNCKit arm64 "$build_tmp"; then
fw_arm="$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework"
have_arm=1
elif fw_arm=$(xc_build_framework_one "$PWD" RoyalVNCKit arm64 "$build_tmp" RoyalVNCKit); then
have_arm=1
fi
else else
if xc_archive_one "$PWD" RoyalVNCKit x86_64 "$build_tmp"; then have_x86=1; fi if xc_archive_one "$PWD" RoyalVNCKit x86_64 "$build_tmp"; then
fw_x86="$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework"
have_x86=1
elif fw_x86=$(xc_build_framework_one "$PWD" RoyalVNCKit x86_64 "$build_tmp" RoyalVNCKit); then
have_x86=1
fi
fi fi
done done
local out_xc="$DEPS_OUT_DIR/RoyalVNCKit.xcframework" local out_xc="$DEPS_OUT_DIR/RoyalVNCKit.xcframework"
rm -rf "$out_xc" rm -rf "$out_xc"
if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \ log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \ -framework "$fw_arm" \
-framework "$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \ -framework "$fw_x86" \
-output "$out_xc" -output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then elif [[ $have_arm -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \ log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/RoyalVNCKit-macos-arm64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \ -framework "$fw_arm" \
-output "$out_xc" -output "$out_xc"
elif [[ $have_x86 -eq 1 ]]; then elif [[ $have_x86 -eq 1 ]]; then
log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \ log_exec "$DEPS_LOGS_DIR/royalvnc-create-xcframework.log" xcodebuild -create-xcframework \
-framework "$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \ -framework "$fw_x86" \
-output "$out_xc" -output "$out_xc"
else else
echo "[deps] error: failed to archive RoyalVNCKit for any macOS arch" >&2 echo "[deps] error: failed to build RoyalVNCKit for any macOS arch" >&2
exit 1 exit 1
fi fi
export ROYALVNCKIT_XCFRAMEWORK="$out_xc" export ROYALVNCKIT_XCFRAMEWORK="$out_xc"
echo "[deps] Built RoyalVNCKit.xcframework at $ROYALVNCKIT_XCFRAMEWORK" echo "[deps] Built RoyalVNCKit.xcframework at $ROYALVNCKIT_XCFRAMEWORK"
# Provide a stable path within the package for SwiftPM binaryTarget resolution
local vendor_dir="$ROOT_DIR/Vendor"
mkdir -p "$vendor_dir"
rm -rf "$vendor_dir/RoyalVNCKit.xcframework"
cp -R "$ROYALVNCKIT_XCFRAMEWORK" "$vendor_dir/"
echo "[deps] Mirrored RoyalVNCKit.xcframework to $vendor_dir"
popd >/dev/null popd >/dev/null
} }
prepare_dependencies() { prepare_dependencies() {
ensure_dirs ensure_dirs
local want_irc="${PROLE_ENABLE_IRCKit:-0}" echo "[deps] (deprecated) RoyalVNCKit manual build not required; managed by SwiftPM"
local want_vnc="${PROLE_ENABLE_RoyalVNCKit:-0}"
echo "[deps] Preparing frameworks — IRCKit: ${want_irc} (0=off,1=on), RoyalVNCKit: ${want_vnc} (0=off,1=on)"
if [[ "$want_irc" = "1" ]]; then
build_irckit_xcframework
if [[ ! -d "${IRCKIT_XCFRAMEWORK:-}" ]]; then
echo "[deps] error: IRCKit.xcframework is missing" >&2; exit 1; fi
else
echo "[deps] IRCKit disabled (PROLE_ENABLE_IRCKit=0); skipping build"
IRCKIT_XCFRAMEWORK=""
fi
if [[ "$want_vnc" = "1" ]]; then
build_royalvnc_xcframework
if [[ ! -d "${ROYALVNCKIT_XCFRAMEWORK:-}" ]]; then
echo "[deps] error: RoyalVNCKit.xcframework is missing" >&2; exit 1; fi
else
echo "[deps] RoyalVNCKit disabled (PROLE_ENABLE_RoyalVNCKit=0); skipping build"
ROYALVNCKIT_XCFRAMEWORK=""
fi
} }
cleanup_dependencies() { cleanup_dependencies() {
@ -552,52 +431,29 @@ cleanup_dependencies() {
fi fi
} }
swiftc_compile() { # Build via Swift Package Manager and return path to built binary on stdout
local arch="$1"; shift spm_build_binary() {
local out="$1"; shift local arch="$1"
echo "[spm] Building (Release) for arch=${arch}" >&2
# Map arch to -target # ROOT_DIR already points to prole-app; build from there so Package.swift is visible.
local target="${arch}-apple-macosx${MIN_MACOS}" pushd "$ROOT_DIR" >/dev/null || return 1
mkdir -p "$LOG_DIR"
echo "[swiftc] Building for ${arch} -> ${out}" # Build via SwiftPM (Package.swift in prole-app). All dependencies managed by SPM.
local args=( build -c release --arch "$arch" \
# Optional linking of frameworks based on flags -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \
local want_irc="${PROLE_ENABLE_IRCKit:-0}" )
local want_vnc="${PROLE_ENABLE_RoyalVNCKit:-0}" # Run swift build quietly; write all output to the log to avoid contaminating stdout
local irc_flags=() swift "${args[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; }
local vnc_flags=() popd >/dev/null
local swift_cfg_flags=() local candidate1="$ROOT_DIR/.build/${arch}-apple-macosx/release/${APP_NAME}"
if [[ "$want_irc" = "1" && -n "${IRCKIT_XCFRAMEWORK:-}" && -d "${IRCKIT_XCFRAMEWORK}" ]]; then local candidate2="$ROOT_DIR/.build/release/${APP_NAME}"
irc_flags=( -F "${IRCKIT_XCFRAMEWORK}" -framework IRCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" ) if [[ -x "$candidate1" ]]; then
swift_cfg_flags+=( -D PROLE_ENABLE_IRCKit ) echo "$candidate1"; return 0
echo "[swiftc] Linking IRCKit from ${IRCKIT_XCFRAMEWORK}"
else
echo "[swiftc] IRCKit disabled or not present; not linking"
# Define a convenience symbol for conditional compilation if app code uses it
swift_cfg_flags+=( -D PROLE_ENABLE_IRCKit_DISABLED )
fi fi
if [[ "$want_vnc" = "1" && -n "${ROYALVNCKIT_XCFRAMEWORK:-}" && -d "${ROYALVNCKIT_XCFRAMEWORK}" ]]; then if [[ -x "$candidate2" ]]; then
vnc_flags=( -F "${ROYALVNCKIT_XCFRAMEWORK}" -framework RoyalVNCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" ) echo "$candidate2"; return 0
swift_cfg_flags+=( -D PROLE_ENABLE_RoyalVNCKit )
echo "[swiftc] Linking RoyalVNCKit from ${ROYALVNCKIT_XCFRAMEWORK}"
else
echo "[swiftc] RoyalVNCKit disabled or not present; not linking"
swift_cfg_flags+=( -D PROLE_ENABLE_RoyalVNCKit_DISABLED )
fi fi
return 1
# shellcheck disable=SC2046
log_exec "$LOG_DIR/swiftc-${arch}.log" xcrun swiftc \
-target "$target" \
-O \
-sdk "$(xcrun --show-sdk-path --sdk macosx)" \
-framework AppKit \
-framework Carbon \
-framework Network \
${swift_cfg_flags[@]:-} \
${irc_flags[@]:-} \
${vnc_flags[@]:-} \
$(/usr/bin/find "$SRC_DIR" -name "*.swift" | sort) \
-o "$out"
} }
codesign_app() { codesign_app() {
@ -605,95 +461,32 @@ codesign_app() {
xcrun codesign --force --deep -s - "$APP_DIR" xcrun codesign --force --deep -s - "$APP_DIR"
} }
embed_irckit_framework() {
if [[ "${PROLE_ENABLE_IRCKit:-0}" != "1" ]]; then
return 0
fi
# Embed IRCKit.xcframework slice into Contents/Frameworks if available
if [[ -z "${IRCKIT_XCFRAMEWORK:-}" ]]; then
return 0
fi
if [[ ! -d "${IRCKIT_XCFRAMEWORK}" ]]; then
echo "[embed] IRCKIT_XCFRAMEWORK path not found: ${IRCKIT_XCFRAMEWORK}"
return 0
fi
local slice="" # Copy SwiftPM-produced dynamic libraries (e.g., libRoyalVNCKit.dylib) into the app bundle
# Prefer universal slice if available; otherwise match host arch embed_spm_dylibs() {
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework" ]]; then local arch="$1"
slice="${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework" local spm_lib_dir="$ROOT_DIR/.build/${arch}-apple-macosx/release"
else if [[ ! -d "$spm_lib_dir" ]]; then
case "$ARCH_CURRENT" in spm_lib_dir="$ROOT_DIR/.build/release"
arm64)
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-arm64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-arm64/IRCKit.framework"
fi
;;
x86_64)
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-x86_64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-x86_64/IRCKit.framework"
fi
;;
esac
fi fi
mkdir -p "$CONTENTS_DIR/Frameworks"
if [[ -z "$slice" ]]; then local found=0
echo "[embed] Could not find matching IRCKit.framework slice in xcframework" if compgen -G "$spm_lib_dir/*.dylib" > /dev/null; then
return 0 for dyl in "$spm_lib_dir"/*.dylib; do
found=1
echo "[embed] Copying $(basename "$dyl") into Frameworks"
cp -f "$dyl" "$CONTENTS_DIR/Frameworks/"
done
fi
if [[ $found -eq 1 ]]; then
echo "[embed] Codesigning embedded dylibs"
# Sign all copied dylibs
find "$CONTENTS_DIR/Frameworks" -name "*.dylib" -print0 | while IFS= read -r -d '' f; do
xcrun codesign --force -s - "$f"
done
fi fi
local dest="$CONTENTS_DIR/Frameworks/IRCKit.framework"
rm -rf "$dest"
echo "[embed] Embedding IRCKit.framework slice: $slice"
cp -R "$slice" "$dest"
# Sign the embedded framework
xcrun codesign --force -s - "$dest"
} }
embed_royalvnckit_framework() {
if [[ "${PROLE_ENABLE_RoyalVNCKit:-0}" != "1" ]]; then
return 0
fi
# Embed RoyalVNCKit.xcframework slice into Contents/Frameworks if available
if [[ -z "${ROYALVNCKIT_XCFRAMEWORK:-}" ]]; then
return 0
fi
if [[ ! -d "${ROYALVNCKIT_XCFRAMEWORK}" ]]; then
echo "[embed] ROYALVNCKIT_XCFRAMEWORK path not found: ${ROYALVNCKIT_XCFRAMEWORK}"
return 0
fi
local slice=""
# Prefer universal slice if present; otherwise pick matching arch
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64_x86_64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64_x86_64/RoyalVNCKit.framework"
else
case "$ARCH_CURRENT" in
arm64)
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-arm64/RoyalVNCKit.framework"
fi
;;
x86_64)
if [[ -d "${ROYALVNCKIT_XCFRAMEWORK}/macos-x86_64/RoyalVNCKit.framework" ]]; then
slice="${ROYALVNCKIT_XCFRAMEWORK}/macos-x86_64/RoyalVNCKit.framework"
fi
;;
esac
fi
if [[ -z "$slice" ]]; then
echo "[embed] Could not find matching RoyalVNCKit.framework slice in xcframework"
return 0
fi
local dest="$CONTENTS_DIR/Frameworks/RoyalVNCKit.framework"
rm -rf "$dest"
echo "[embed] Embedding RoyalVNCKit.framework slice: $slice"
cp -R "$slice" "$dest"
# Sign the embedded framework
xcrun codesign --force -s - "$dest"
}
build_one_arch() { build_one_arch() {
local arch="$1" local arch="$1"
@ -707,17 +500,22 @@ build_one_arch() {
gen_app_icns gen_app_icns
gen_statusbar_icon_png gen_statusbar_icon_png
copy_extra_resources copy_extra_resources
local bin="$BUILD_DIR/${APP_NAME}-${arch}" local built_bin
swiftc_compile "$arch" "$bin" built_bin=$(spm_build_binary "$arch") || { echo "[spm] build failed" >&2; exit 1; }
mkdir -p "$MACOS_DIR" mkdir -p "$MACOS_DIR"
cp "$bin" "$MACOS_DIR/${APP_NAME}" cp "$built_bin" "$MACOS_DIR/${APP_NAME}"
embed_irckit_framework # Embed any SwiftPM dynamic libs (e.g., RoyalVNCKit) into the app bundle
embed_royalvnckit_framework embed_spm_dylibs "$arch"
# Ensure the app binary can locate embedded dylibs in Contents/Frameworks at runtime
echo "[rpath] Adding @executable_path/../Frameworks to app binary rpaths"
xcrun install_name_tool -add_rpath "@executable_path/../Frameworks" "$MACOS_DIR/${APP_NAME}" 2>/dev/null || true
codesign_app codesign_app
echo "Built: $APP_DIR" echo "Built: $APP_DIR"
echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)" echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)"
echo "[summary] Embedded frameworks:" echo "[summary] Embedded frameworks (.framework):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /' /usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies cleanup_dependencies
} }
@ -729,19 +527,17 @@ build_universal() {
prepare_dependencies prepare_dependencies
ensure_dirs ensure_dirs
gen_plist gen_plist
local bin_arm64="$BUILD_DIR/${APP_NAME}-arm64" local built_arm64; built_arm64=$(spm_build_binary arm64) || { echo "[spm] arm64 build failed" >&2; exit 1; }
local bin_x86="$BUILD_DIR/${APP_NAME}-x86_64" local built_x86; built_x86=$(spm_build_binary x86_64) || { echo "[spm] x86_64 build failed" >&2; exit 1; }
swiftc_compile arm64 "$bin_arm64"
swiftc_compile x86_64 "$bin_x86"
mkdir -p "$MACOS_DIR" mkdir -p "$MACOS_DIR"
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$bin_arm64" "$bin_x86" xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$built_arm64" "$built_x86"
embed_irckit_framework
embed_royalvnckit_framework
codesign_app codesign_app
echo "Built universal: $APP_DIR" echo "Built universal: $APP_DIR"
echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)" echo "[summary] App binary: $(lipo -info "$MACOS_DIR/${APP_NAME}" 2>/dev/null || echo unknown)"
echo "[summary] Embedded frameworks:" echo "[summary] Embedded frameworks (.framework):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /' /usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type d -name "*.framework" -exec basename {} \; | sed 's/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies cleanup_dependencies
} }

View File

@ -37,22 +37,6 @@ status=$?
set -e set -e
echo "[debug] build.sh finished with status: $status" echo "[debug] build.sh finished with status: $status"
echo
echo "[debug] ===== Dependency logs (IRCKit) ====="
for f in \
"$DEPS_LOGS_DIR/irckit-archive-arm64.log" \
"$DEPS_LOGS_DIR/irckit-archive-x86_64.log" \
"$DEPS_LOGS_DIR/irckit-create-xcframework.log" \
"$DEPS_LOGS_DIR/irckit-spm-update.log" \
"$DEPS_LOGS_DIR/irckit-spm-resolve.log" \
; do
if [[ -f "$f" ]]; then
echo "----- tail: ${f} -----"
tail -n 200 "$f" || true
echo
fi
done
echo "[debug] ===== Dependency logs (RoyalVNCKit) =====" echo "[debug] ===== Dependency logs (RoyalVNCKit) ====="
for f in \ for f in \
"$DEPS_LOGS_DIR/royalvnc-archive-arm64.log" \ "$DEPS_LOGS_DIR/royalvnc-archive-arm64.log" \