### 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 397cd76328
8 changed files with 326 additions and 616 deletions

View File

@ -19,7 +19,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
private var hotKeyManager: HotKeyManager!
private var serviceChecker: ServiceChecker!
private var statusItemController: StatusItemController!
private var splashTipController: SplashTipWindowController?
// Splash screen removed keep no reference
private var appMenuToggleStatusBarItem: NSMenuItem?
private var pfManager: PortForwardManager?
@ -37,10 +37,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
NSApp.setActivationPolicy(.regular)
dlog("Activation policy set to .regular (starting in App Window mode)")
// Show startup tip splash for 5 seconds with animated GIF and counter
let splash = SplashTipWindowController()
self.splashTipController = splash
splash.show()
// Splash removed: start directly
// ServiceChecker does the lightweight TCP checks on a background timer
serviceChecker = ServiceChecker()
@ -114,15 +111,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
dlog("Starting ServiceChecker timer (interval: 30s)")
serviceChecker.start(interval: 30)
// Release the splash controller reference shortly after it is expected to auto-dismiss
DispatchQueue.main.asyncAfter(deadline: .now() + 6.0) { [weak self] in
self?.splashTipController = nil
}
// No splash controller to release
// Build application menu (shown when activation policy is .regular)
setupApplicationMenu()
// 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()
// Ensure menu reflects current visibility
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 localPort: Int { int("k3d.local.port", default: 6443) }
// UI asset config
// 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
}
// UI background config was removed along with the splash screen.
// Dev port-forward supervision
var pfEnabled: Bool {

View File

@ -1,7 +1,6 @@
import Foundation
import AppKit
#if canImport(IRCKit)
import IRCKit
#endif
import IRC
// A secondary window for IRC chat: short vertically, long horizontally.
// 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 kSSLKey = "irc.ssl"
#if canImport(IRCKit)
private var client: IRCClient?
#endif
private var ircClient: IRCClient?
private var joinedDefaultChannel = false
init(initialServer: String? = nil, initialSSL: Bool? = nil) {
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)
super.init(window: window)
@ -54,6 +53,7 @@ final class IRCWindowController: NSWindowController {
window.level = .normal
window.collectionBehavior = [.canJoinAllSpaces]
window.appearance = NSAppearance(named: .aqua)
window.minSize = NSSize(width: 480, height: 200)
// Restore persisted values
let savedServer = initialServer ?? defaults.string(forKey: kServerKey) ?? "localhost"
@ -74,10 +74,15 @@ final class IRCWindowController: NSWindowController {
topBar.translatesAutoresizingMaskIntoConstraints = false
content.addSubview(topBar)
// Fix server text field width approximately to 32 characters
let charWidth: CGFloat = 8.0 // approx for monospaced at system size
let targetWidth: CGFloat = charWidth * 32.0
serverField.widthAnchor.constraint(greaterThanOrEqualToConstant: targetWidth).isActive = true
// Make the server field flexible (no fixed minimum). It should stretch to fill available space.
serverField.setContentHuggingPriority(.defaultLow, for: .horizontal)
serverField.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)
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(serverField)
@ -99,7 +104,8 @@ final class IRCWindowController: NSWindowController {
NSLayoutConstraint.activate([
topBar.topAnchor.constraint(equalTo: topAnchorRef, 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.leadingAnchor.constraint(equalTo: leadingAnchorRef),
@ -111,11 +117,8 @@ final class IRCWindowController: NSWindowController {
sslCheckbox.target = self; sslCheckbox.action = #selector(didToggleSSL)
connectButton.target = self; connectButton.action = #selector(didTapConnect)
#if !canImport(IRCKit)
connectButton.isEnabled = false
connectButton.toolTip = "IRCKit not linked — cannot connect"
transcriptView.appendLine("IRCKit not linked — chat disabled.")
#endif
connectButton.isEnabled = true
connectButton.toolTip = "Connect to #prole"
}
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 }
let refFrame = refWindow.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.y = refFrame.origin.y - newFrame.height - gap
this.setFrame(newFrame, display: true, animate: false)
@ -177,56 +182,88 @@ final class IRCWindowController: NSWindowController {
}
private func connectTo(host: String, port: Int, ssl: Bool) {
transcriptView.appendLine("Connecting to \(host):\(port) \(ssl ? "(SSL)" : "")")
#if canImport(IRCKit)
// Disconnect if already connected
client?.disconnect(message: "reconnect")
// IRCKit API surface (approximate based on typical IRC libs). Adjust if needed.
// Note: TLS/SSL is not yet supported by the swift-nio-irc-client package here.
if ssl {
transcriptView.appendLine("Note: SSL/TLS not implemented in current IRC client; attempting plain connection…")
}
let nick = nickname()
let user = nick
let realName = "Prole"
let configuration = IRCConfiguration(
hostname: host,
port: UInt16(port),
secure: ssl,
nickname: nick,
username: user,
realname: realName
let options = IRCClientOptions(
port: port,
host: host,
password: nil,
nickname: IRCNickName(nick)!,
userInfo: IRCUserInfo(username: nick, hostname: host, servername: host, realname: "Prole")
)
let client = IRCClient(options: options)
self.ircClient = client
self.joinedDefaultChannel = false
let client = IRCClient(configuration: configuration)
self.client = client
// Delegate callbacks
class Delegate: IRCClientDelegate {
weak var owner: IRCWindowController?
init(owner: IRCWindowController) { self.owner = owner }
client.onConnect = { [weak self] in
self?.transcriptView.appendLine("Connected. Joining #prole …")
client.join(channel: "#prole")
}
client.onDisconnect = { [weak self] reason, _ in
self?.transcriptView.appendLine("Disconnected: \(reason ?? "unknown")")
}
client.onMessage = { [weak self] message in
guard let self = self else { return }
let nick = message.sender?.nickname ?? "?"
let text = message.message
self.transcriptView.appendLine("<\(nick)> \(text)")
}
client.onNotice = { [weak self] notice in
self?.transcriptView.appendLine("-notice- \(notice.message)")
}
client.onError = { [weak self] err in
self?.transcriptView.appendLine("Error: \(err.localizedDescription)")
func client(_ client: IRCClient, registered nick: IRCNickName, with userInfo: IRCUserInfo) {
owner?.transcriptView.appendLine("Registered as \(nick.stringValue)")
// Join default channel
if owner?.joinedDefaultChannel == false {
client.send(.otherCommand("JOIN", ["#prole"]))
owner?.joinedDefaultChannel = true
}
}
func clientFailedToRegister(_ client: IRCClient) {
owner?.transcriptView.appendLine("Failed to register with server")
}
func client(_ client: IRCClient, received message: IRCMessage) {
// Render a few common messages
switch message.command {
case .PRIVMSG(let target, let text):
// The upstream message model may not expose a prefix property consistently across versions.
// Fallback to unknown sender for now.
let from = "?"
if case .channel(let ch) = target.first {
owner?.transcriptView.appendLine("[\(ch)] <\(from)> \(text)")
} else {
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()
#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") }
convenience init() {
self.init(frame: .zero)
}
func appendLine(_ line: String) {
let ts = IRCTranscriptView.timestamp()
let s = "[\(ts)] \(line)\n"

View File

@ -34,8 +34,7 @@ final class MainWindowController: NSWindowController {
super.init(window: window)
window.isReleasedWhenClosed = false
window.title = "ProleStatus"
window.center()
window.title = "Prole Status — ⌘⌥⇧P to toggle"
window.level = .normal
window.collectionBehavior = [.canJoinAllSpaces]
window.appearance = NSAppearance(named: .aqua)
@ -125,6 +124,7 @@ final class MainWindowController: NSWindowController {
func show() {
guard let window = window else { return }
MainWindowController.positionWindowAtLeftEdge(window)
window.makeKeyAndOrderFront(nil)
NSApp.activate(ignoringOtherApps: false)
}
@ -135,6 +135,20 @@ final class MainWindowController: NSWindowController {
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
@objc private func didTapRefresh() { actions.onRefresh() }
@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
#if canImport(RoyalVNCKit)
import RoyalVNCKit
#endif
// A secondary window that hosts the Prole Workstation (VNC viewer)
// 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 {
private let placeholder: NSTextField = {
let tf = NSTextField(labelWithString: "VNC viewer unavailable — RoyalVNCKit not linked")
tf.alignment = .center
tf.textColor = .secondaryLabelColor
tf.font = NSFont.systemFont(ofSize: 13)
tf.translatesAutoresizingMaskIntoConstraints = false
return tf
}()
private var framebufferView: VNCCAFramebufferView?
private var connection: VNCConnection?
// Keep a strong reference to the delegate; VNCConnection holds it weakly
private var connectionDelegateStrongRef: VNCConnectionDelegate?
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
wantsLayer = true
layer?.backgroundColor = NSColor.windowBackgroundColor.cgColor
#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
// The actual framebuffer view will be created when the connection creates the framebuffer
}
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) {
#if canImport(RoyalVNCKit)
// The concrete API follows the RoyalVNCKit USAGE; adjust if needed when linking.
let settings = VNCConnection.Settings(hostname: hostname, port: UInt16(port))
let connection = VNCConnection(settings: settings)
let vncView = VNCView(frame: bounds)
vncView.translatesAutoresizingMaskIntoConstraints = false
addSubview(vncView)
NSLayoutConstraint.activate([
vncView.topAnchor.constraint(equalTo: topAnchor),
vncView.leadingAnchor.constraint(equalTo: leadingAnchor),
vncView.trailingAnchor.constraint(equalTo: trailingAnchor),
vncView.bottomAnchor.constraint(equalTo: bottomAnchor)
])
vncView.connect(connection)
#else
// Nothing to do; placeholder already shown
_ = hostname; _ = port
#endif
// Construct full settings according to current RoyalVNCKit API
let settings = VNCConnection.Settings(
isDebugLoggingEnabled: false,
hostname: hostname,
port: UInt16(port),
isShared: true,
isScalingEnabled: true,
useDisplayLink: true,
inputMode: .forwardKeyboardShortcutsIfNotInUseLocally,
isClipboardRedirectionEnabled: true,
colorDepth: .depth24Bit,
frameEncodings: .default
)
let conn = VNCConnection(settings: settings)
self.connection = conn
// 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
# 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"
# Deps staging directories
@ -56,16 +55,11 @@ Examples:
./build.sh package
Dependencies:
This script can download, build, link, and embed IRCKit (0.16.0)
and RoyalVNCKit (1.0.1). These are optional and controlled by flags below.
IRC and RoyalVNC are built via Swift Package Manager.
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
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 Swift6compatible 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
}
@ -223,29 +217,6 @@ PLIST
}
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
local props_src="$ROOT_DIR/prole.properties"
if [[ -f "$props_src" ]]; then
@ -256,7 +227,7 @@ copy_extra_resources() {
fi
}
# --- Dependency preparation (mandatory IRCKit & RoyalVNCKit) ---
# --- Dependency preparation (RoyalVNCKit) ---
have_cmd() { command -v "$1" >/dev/null 2>&1; }
@ -295,22 +266,19 @@ xc_archive_one() {
proj_opt=( -project "${scheme}.xcodeproj" )
fi
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
pushd "$proj_dir" >/dev/null || return 1
# Per-scheme extra flags / xcconfig
local extra_args=()
if [[ "$scheme" == "IRCKit" && -n ${PROLE_DISABLE_SSL:-} ]]; then
echo "[deps] PROLE_DISABLE_SSL=1: building IRCKit with PROLE_DISABLE_SSL compilation condition"
# Use a temporary xcconfig to avoid shell/argument parsing issues on the command line
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)
if [[ "$scheme" == "RoyalVNCKit" ]]; then
# Enable distribution interfaces so SPM can import the Swift module, but disable verification to avoid toolchain issues
local xcflags_file="$outdir/royalvnc-archive-overrides.xcconfig"
cat > "$xcflags_file" <<'XCCONFIG'
SWIFT_ACTIVE_COMPILATION_CONDITIONS = $(inherited) PROLE_DISABLE_SSL
OTHER_SWIFT_FLAGS = $(inherited) -DPROLE_DISABLE_SSL
SWIFT_VERSION = 5.0
SWIFT_STRICT_CONCURRENCY = minimal
BUILD_LIBRARY_FOR_DISTRIBUTION = YES
SWIFT_EMIT_MODULE_INTERFACE = YES
SWIFT_SERIALIZE_DEBUGGING_OPTIONS = NO
OTHER_SWIFT_FLAGS = $(inherited) -no-verify-emitted-module-interface
XCCONFIG
extra_args+=( -xcconfig "$xcflags_file" )
fi
@ -331,134 +299,47 @@ XCCONFIG
popd >/dev/null
}
build_irckit_xcframework() {
# If external override exists, trust it
if [[ -n ${IRCKIT_XCFRAMEWORK:-} && -d ${IRCKIT_XCFRAMEWORK} ]]; then
echo "[deps] Using provided IRCKit.xcframework: ${IRCKIT_XCFRAMEWORK}"
# Build one framework w/ xcodebuild (not archive), outputting a .framework into outdir
xc_build_framework_one() {
local proj_dir="$1"; local scheme="$2"; local arch="$3"; local outdir="$4"; local product_name="$5"
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
fi
local work="$DEPS_SRC_DIR/IRCKit-src"
local tar="$DEPS_DIR/irckit.tar.gz"
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"
if [[ -d "$build_dir/PackageFrameworks/${product_name}.framework" ]]; then
echo "$build_dir/PackageFrameworks/${product_name}.framework"
return 0
fi
# 2) Else pick the first child directory that contains a Package.swift
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 Swift5compatible 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, Swift5compatible version to avoid Swift6only 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 Swift5compatible 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
return 1
}
build_royalvnc_xcframework() {
@ -481,6 +362,7 @@ build_royalvnc_xcframework() {
local build_tmp="$DEPS_OUT_DIR/RoyalVNC-build"
rm -rf "$build_tmp"; mkdir -p "$build_tmp"
local have_arm=0; local have_x86=0
local fw_arm=""; local fw_x86=""
local want_arches=(arm64 x86_64)
if [[ "${PROLE_BUILD_ARCH:-}" == "arm64" ]]; then
want_arches=(arm64)
@ -489,58 +371,55 @@ build_royalvnc_xcframework() {
fi
for dep_arch in "${want_arches[@]}"; do
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
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
done
local out_xc="$DEPS_OUT_DIR/RoyalVNCKit.xcframework"
rm -rf "$out_xc"
if [[ $have_arm -eq 1 && $have_x86 -eq 1 ]]; then
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 "$build_tmp/RoyalVNCKit-macos-x86_64.xcarchive/Products/Library/Frameworks/RoyalVNCKit.framework" \
-framework "$fw_arm" \
-framework "$fw_x86" \
-output "$out_xc"
elif [[ $have_arm -eq 1 ]]; then
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"
elif [[ $have_x86 -eq 1 ]]; then
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"
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
fi
export ROYALVNCKIT_XCFRAMEWORK="$out_xc"
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
}
prepare_dependencies() {
ensure_dirs
local want_irc="${PROLE_ENABLE_IRCKit:-0}"
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
echo "[deps] (deprecated) RoyalVNCKit manual build not required; managed by SwiftPM"
}
cleanup_dependencies() {
@ -552,52 +431,29 @@ cleanup_dependencies() {
fi
}
swiftc_compile() {
local arch="$1"; shift
local out="$1"; shift
# Map arch to -target
local target="${arch}-apple-macosx${MIN_MACOS}"
echo "[swiftc] Building for ${arch} -> ${out}"
# Optional linking of frameworks based on flags
local want_irc="${PROLE_ENABLE_IRCKit:-0}"
local want_vnc="${PROLE_ENABLE_RoyalVNCKit:-0}"
local irc_flags=()
local vnc_flags=()
local swift_cfg_flags=()
if [[ "$want_irc" = "1" && -n "${IRCKIT_XCFRAMEWORK:-}" && -d "${IRCKIT_XCFRAMEWORK}" ]]; then
irc_flags=( -F "${IRCKIT_XCFRAMEWORK}" -framework IRCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" )
swift_cfg_flags+=( -D PROLE_ENABLE_IRCKit )
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 )
# Build via Swift Package Manager and return path to built binary on stdout
spm_build_binary() {
local arch="$1"
echo "[spm] Building (Release) for arch=${arch}" >&2
# ROOT_DIR already points to prole-app; build from there so Package.swift is visible.
pushd "$ROOT_DIR" >/dev/null || return 1
mkdir -p "$LOG_DIR"
# Build via SwiftPM (Package.swift in prole-app). All dependencies managed by SPM.
local args=( build -c release --arch "$arch" \
-Xlinker -rpath -Xlinker "@executable_path/../Frameworks" \
)
# Run swift build quietly; write all output to the log to avoid contaminating stdout
swift "${args[@]}" >"$LOG_DIR/spm-build-${arch}.log" 2>&1 || { popd >/dev/null; return 1; }
popd >/dev/null
local candidate1="$ROOT_DIR/.build/${arch}-apple-macosx/release/${APP_NAME}"
local candidate2="$ROOT_DIR/.build/release/${APP_NAME}"
if [[ -x "$candidate1" ]]; then
echo "$candidate1"; return 0
fi
if [[ "$want_vnc" = "1" && -n "${ROYALVNCKIT_XCFRAMEWORK:-}" && -d "${ROYALVNCKIT_XCFRAMEWORK}" ]]; then
vnc_flags=( -F "${ROYALVNCKIT_XCFRAMEWORK}" -framework RoyalVNCKit -Xlinker -rpath -Xlinker "@executable_path/../Frameworks" )
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 )
if [[ -x "$candidate2" ]]; then
echo "$candidate2"; return 0
fi
# 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"
return 1
}
codesign_app() {
@ -605,95 +461,32 @@ codesign_app() {
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=""
# Prefer universal slice if available; otherwise match host arch
if [[ -d "${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework" ]]; then
slice="${IRCKIT_XCFRAMEWORK}/macos-arm64_x86_64/IRCKit.framework"
else
case "$ARCH_CURRENT" in
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
# Copy SwiftPM-produced dynamic libraries (e.g., libRoyalVNCKit.dylib) into the app bundle
embed_spm_dylibs() {
local arch="$1"
local spm_lib_dir="$ROOT_DIR/.build/${arch}-apple-macosx/release"
if [[ ! -d "$spm_lib_dir" ]]; then
spm_lib_dir="$ROOT_DIR/.build/release"
fi
if [[ -z "$slice" ]]; then
echo "[embed] Could not find matching IRCKit.framework slice in xcframework"
return 0
mkdir -p "$CONTENTS_DIR/Frameworks"
local found=0
if compgen -G "$spm_lib_dir/*.dylib" > /dev/null; then
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
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() {
local arch="$1"
@ -707,17 +500,22 @@ build_one_arch() {
gen_app_icns
gen_statusbar_icon_png
copy_extra_resources
local bin="$BUILD_DIR/${APP_NAME}-${arch}"
swiftc_compile "$arch" "$bin"
local built_bin
built_bin=$(spm_build_binary "$arch") || { echo "[spm] build failed" >&2; exit 1; }
mkdir -p "$MACOS_DIR"
cp "$bin" "$MACOS_DIR/${APP_NAME}"
embed_irckit_framework
embed_royalvnckit_framework
cp "$built_bin" "$MACOS_DIR/${APP_NAME}"
# Embed any SwiftPM dynamic libs (e.g., RoyalVNCKit) into the app bundle
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
echo "Built: $APP_DIR"
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/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies
}
@ -729,19 +527,17 @@ build_universal() {
prepare_dependencies
ensure_dirs
gen_plist
local bin_arm64="$BUILD_DIR/${APP_NAME}-arm64"
local bin_x86="$BUILD_DIR/${APP_NAME}-x86_64"
swiftc_compile arm64 "$bin_arm64"
swiftc_compile x86_64 "$bin_x86"
local built_arm64; built_arm64=$(spm_build_binary arm64) || { echo "[spm] arm64 build failed" >&2; exit 1; }
local built_x86; built_x86=$(spm_build_binary x86_64) || { echo "[spm] x86_64 build failed" >&2; exit 1; }
mkdir -p "$MACOS_DIR"
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$bin_arm64" "$bin_x86"
embed_irckit_framework
embed_royalvnckit_framework
xcrun lipo -create -output "$MACOS_DIR/${APP_NAME}" "$built_arm64" "$built_x86"
codesign_app
echo "Built universal: $APP_DIR"
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/^/ - /'
echo "[summary] Embedded dynamic libraries (.dylib):"
/usr/bin/find "$CONTENTS_DIR/Frameworks" -maxdepth 1 -type f -name "*.dylib" -exec basename {} \; | sed 's/^/ - /'
cleanup_dependencies
}

View File

@ -37,22 +37,6 @@ status=$?
set -e
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) ====="
for f in \
"$DEPS_LOGS_DIR/royalvnc-archive-arm64.log" \