diff --git a/prole-app/Package.swift b/prole-app/Package.swift new file mode 100644 index 0000000..d113758 --- /dev/null +++ b/prole-app/Package.swift @@ -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") + ] + ) + ] +) diff --git a/prole-app/Sources/AppDelegate.swift b/prole-app/Sources/AppDelegate.swift index b7f9ec7..fca139a 100644 --- a/prole-app/Sources/AppDelegate.swift +++ b/prole-app/Sources/AppDelegate.swift @@ -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) diff --git a/prole-app/Sources/Config.swift b/prole-app/Sources/Config.swift index 456e38c..40ff95d 100644 --- a/prole-app/Sources/Config.swift +++ b/prole-app/Sources/Config.swift @@ -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 { diff --git a/prole-app/Sources/IRCWindowController.swift b/prole-app/Sources/IRCWindowController.swift index 2c30001..e128685 100644 --- a/prole-app/Sources/IRCWindowController.swift +++ b/prole-app/Sources/IRCWindowController.swift @@ -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" diff --git a/prole-app/Sources/MainWindowController.swift b/prole-app/Sources/MainWindowController.swift index d81e9a8..a24f23a 100644 --- a/prole-app/Sources/MainWindowController.swift +++ b/prole-app/Sources/MainWindowController.swift @@ -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() } diff --git a/prole-app/Sources/SplashTipWindowController.swift b/prole-app/Sources/SplashTipWindowController.swift deleted file mode 100644 index 7a8e0b4..0000000 --- a/prole-app/Sources/SplashTipWindowController.swift +++ /dev/null @@ -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() - } -} diff --git a/prole-app/Sources/WorkstationWindowController.swift b/prole-app/Sources/WorkstationWindowController.swift index b3d6757..f311032 100644 --- a/prole-app/Sources/WorkstationWindowController.swift +++ b/prole-app/Sources/WorkstationWindowController.swift @@ -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() } } diff --git a/prole-app/build.sh b/prole-app/build.sh index df066e5..203bb6d 100755 --- a/prole-app/build.sh +++ b/prole-app/build.sh @@ -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 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 } @@ -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 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 + 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 } diff --git a/prole-app/debug.sh b/prole-app/debug.sh index 2104685..1070a63 100755 --- a/prole-app/debug.sh +++ b/prole-app/debug.sh @@ -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" \