Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: Test

on:
pull_request:

jobs:
test:
name: Copilot for Xcode Debug
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Select Xcode 26
id: xcode
run: |
shopt -s nullglob
apps=(/Applications/Xcode_26*.app)
if [ ${#apps[@]} -eq 0 ]; then
echo "has_xcode26=false" >> "$GITHUB_OUTPUT"
echo "Xcode 26 not found; using runner default."
else
IFS=$'\n' sorted=($(printf '%s\n' "${apps[@]}" | sort -V))
app="${sorted[${#sorted[@]}-1]}"
sudo xcode-select -s "$app/Contents/Developer"
echo "has_xcode26=true" >> "$GITHUB_OUTPUT"
echo "Selected $app"
fi
xcodebuild -version

- name: Test
continue-on-error: ${{ steps.xcode.outputs.has_xcode26 != 'true' }}
run: |
xcodebuild -workspace "Copilot for Xcode.xcworkspace" -scheme "Copilot for Xcode Debug" \
-configuration Debug -destination 'platform=macOS' -skipMacroValidation \
CODE_SIGNING_ALLOWED=NO \
test -parallel-testing-enabled NO
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ iOSInjectionProject/
https://www.toptal.com/developers/gitignore/api/xcode,macos,swift,swiftpackagemanager

Secrets.xcconfig
Local.xcconfig
Python/Python.xcframework
Python/python-stdlib
Python/site-packages/*
Expand Down
2 changes: 1 addition & 1 deletion ChatPlugins/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import PackageDescription

let package = Package(
name: "ChatPlugins",
platforms: [.macOS(.v12)],
platforms: [.macOS(.v13)],
products: [
.library(
name: "ChatPlugins",
Expand Down
91 changes: 79 additions & 12 deletions CommunicationBridge/ServiceDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,27 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate {
_: NSXPCListener,
shouldAcceptNewConnection newConnection: NSXPCConnection
) -> Bool {
guard let teamID = XPCPeerRequirement.teamID(fromPrefix: teamIDPrefix) else {
Logger.communicationBridge.error(
"Rejected XPC connection from pid \(newConnection.processIdentifier): Team ID is unavailable."
)
return false
}
let requirement = XPCPeerRequirement.codeSigningRequirement(teamID: teamID)
do {
try XPCPeerRequirement.setCodeSigningRequirement(on: newConnection, teamID: teamID)
} catch {
Logger.communicationBridge.error(
"Rejected XPC connection from pid \(newConnection.processIdentifier): invalid code signing requirement (\(requirement))."
)
return false
}

newConnection.exportedInterface = NSXPCInterface(
with: CommunicationBridgeXPCServiceProtocol.self
)

let exportedObject = XPCService()
let exportedObject = XPCService(processIdentifier: newConnection.processIdentifier)
newConnection.exportedObject = exportedObject
newConnection.resume()

Expand All @@ -24,6 +40,11 @@ class ServiceDelegate: NSObject, NSXPCListenerDelegate {

class XPCService: CommunicationBridgeXPCServiceProtocol {
static let eventHandler = EventHandler()
let processIdentifier: pid_t

init(processIdentifier: pid_t) {
self.processIdentifier = processIdentifier
}

func launchExtensionServiceIfNeeded(
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
Expand All @@ -44,13 +65,18 @@ class XPCService: CommunicationBridgeXPCServiceProtocol {
withReply reply: @escaping () -> Void
) {
Task {
await Self.eventHandler.updateServiceEndpoint(endpoint: endpoint, withReply: reply)
await Self.eventHandler.updateServiceEndpoint(
endpoint: endpoint,
processIdentifier: processIdentifier,
withReply: reply
)
}
}
}

actor EventHandler {
var endpoint: NSXPCListenerEndpoint?
var endpointPID: pid_t?
let launcher = ExtensionServiceLauncher()
var exitTask: Task<Void, Error>?

Expand All @@ -62,21 +88,27 @@ actor EventHandler {
withReply reply: @escaping (NSXPCListenerEndpoint?) -> Void
) async {
rescheduleExitTask()
#if DEBUG
if let endpoint, !(await testXPCListenerEndpoint(endpoint)) {
self.endpoint = nil
if let endpoint,
let pid = endpointPID,
let running = NSRunningApplication(processIdentifier: pid),
!running.isTerminated
{
Logger.communicationBridge.info("Service app is still valid")
await launcher.attach(running)
reply(endpoint)
return
}
reply(endpoint)
#else
// Stale anonymous listener: do not keep a handle just because some other
// process with the same bundle id is still running.
endpoint = nil
endpointPID = nil
if await launcher.isApplicationValid {
Logger.communicationBridge.info("Service app is still valid")
reply(endpoint)
reply(nil)
} else {
endpoint = nil
await launcher.launch()
reply(nil)
}
#endif
}

func quit(withReply reply: () -> Void) {
Expand All @@ -85,9 +117,24 @@ actor EventHandler {
exit(0)
}

func updateServiceEndpoint(endpoint: NSXPCListenerEndpoint, withReply reply: () -> Void) {
func updateServiceEndpoint(
endpoint: NSXPCListenerEndpoint,
processIdentifier: pid_t,
withReply reply: () -> Void
) {
rescheduleExitTask()
let expectedBundleID = bundleIdentifierBase + ".ExtensionService"
let actualBundleID = NSRunningApplication(processIdentifier: processIdentifier)?
.bundleIdentifier
guard actualBundleID == expectedBundleID else {
Logger.communicationBridge.error(
"Ignoring service endpoint from pid \(processIdentifier) (\(actualBundleID ?? "unknown bundle id")); expected \(expectedBundleID)."
)
reply()
return
}
self.endpoint = endpoint
endpointPID = processIdentifier
reply()
}

Expand Down Expand Up @@ -131,7 +178,16 @@ actor ExtensionServiceLauncher {
return false
}

func attach(_ application: NSRunningApplication) {
self.application = application
}

func launch() {
if let running = runningApplicationMatchingAppURL() {
application = running
return
}

guard !isLaunching else { return }
isLaunching = true

Expand Down Expand Up @@ -161,5 +217,16 @@ actor ExtensionServiceLauncher {
self.isLaunching = false
}
}
}

/// Prefer the instance launched from this package; other installs with the same
/// bundle id fall through to `openApplication(at: appURL)`.
private func runningApplicationMatchingAppURL() -> NSRunningApplication? {
let wanted = appURL.standardizedFileURL
return NSRunningApplication.runningApplications(withBundleIdentifier: appIdentifier)
.first { running in
guard !running.isTerminated else { return false }
guard let bundleURL = running.bundleURL else { return false }
return bundleURL.standardizedFileURL == wanted
}
}
}
8 changes: 6 additions & 2 deletions CommunicationBridge/main.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,13 @@ import Foundation

class AppDelegate: NSObject, NSApplicationDelegate {}

let bundleIdentifierBase = Bundle(url: Bundle.main.bundleURL.appendingPathComponent(
let extensionServiceBundle = Bundle(url: Bundle.main.bundleURL.appendingPathComponent(
"CopilotForXcodeExtensionService.app"
))?.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as? String ?? "com.intii.CopilotForXcode"
))
let bundleIdentifierBase = extensionServiceBundle?
.object(forInfoDictionaryKey: "BUNDLE_IDENTIFIER_BASE") as? String ?? "com.intii.CopilotForXcode"
let teamIDPrefix = extensionServiceBundle?
.object(forInfoDictionaryKey: "TEAM_ID_PREFIX") as? String

let serviceIdentifier = bundleIdentifierBase + ".CommunicationBridge"
let appDelegate = AppDelegate()
Expand Down
11 changes: 11 additions & 0 deletions Config.debug.xcconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#include "Version.xcconfig"
SLASH = /

// Apple Silicon only: no Intel slice is built, so no target can fall back to
// ARCHS_STANDARD and quietly reintroduce x86_64.
ARCHS = arm64

// Belt and braces: Xcode re-materializes ARCHS = $(ARCHS_STANDARD) at target level whenever the
// Architectures row is touched in the UI, which silently overrides the line above. EXCLUDED_ARCHS
// is subtracted from whatever ARCHS ends up being, so the Intel slice stays out either way.
EXCLUDED_ARCHS = x86_64

HOST_APP_NAME = Copilot for Xcode Dev
BUNDLE_IDENTIFIER_BASE = dev.com.intii.CopilotForXcode
SPARKLE_FEED_URL = http:$(SLASH)$(SLASH)127.0.0.1:9433/appcast.xml
Expand All @@ -11,3 +20,5 @@ EXTENSION_BUNDLE_DISPLAY_NAME = Copilot Dev
EXTENSION_SERVICE_NAME = CopilotForXcodeExtensionService

// see also target Configs

#include? "Local.xcconfig"
11 changes: 11 additions & 0 deletions Config.xcconfig
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
#include "Version.xcconfig"
SLASH = /

// Apple Silicon only: no Intel slice is built, so no target can fall back to
// ARCHS_STANDARD and quietly reintroduce x86_64.
ARCHS = arm64

// Belt and braces: Xcode re-materializes ARCHS = $(ARCHS_STANDARD) at target level whenever the
// Architectures row is touched in the UI, which silently overrides the line above. EXCLUDED_ARCHS
// is subtracted from whatever ARCHS ends up being, so the Intel slice stays out either way.
EXCLUDED_ARCHS = x86_64

HOST_APP_NAME = Copilot for Xcode
BUNDLE_IDENTIFIER_BASE = com.intii.CopilotForXcode
SPARKLE_FEED_URL = https:$(SLASH)$(SLASH)copilotforxcode.intii.com/appcast.xml
Expand All @@ -11,3 +20,5 @@ EXTENSION_BUNDLE_DISPLAY_NAME = Copilot
EXTENSION_SERVICE_NAME = CopilotForXcodeExtensionService

// see also target Configs

#include? "Local.xcconfig"
Loading