@macify/xpc 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app.plugin.js +1 -0
- package/build/index.d.cts +33 -0
- package/expo-module.config.json +6 -0
- package/ios/MacifyServiceClient.swift +152 -0
- package/ios/MacifyXPC.podspec +18 -0
- package/ios/MacifyXPCModule.swift +85 -0
- package/ios/MacifyXPCProtocol.swift +16 -0
- package/ios/XPCConnectionFactory.h +14 -0
- package/ios/XPCConnectionFactory.m +17 -0
- package/package.json +10 -1
- package/plugin/build/index.cjs +431 -0
- package/plugin/build/index.d.cts +17 -0
- package/swift/Package.swift +19 -0
- package/swift/Sources/MacifyXPCService/MacifyServiceImpl.swift +51 -0
- package/swift/Sources/MacifyXPCService/MacifyXPCError.swift +22 -0
- package/swift/Sources/MacifyXPCService/MacifyXPCProtocol.swift +21 -0
- package/swift/Sources/MacifyXPCService/XPCEventEmitter.swift +29 -0
- package/swift/Sources/MacifyXPCService/XPCMessageHandler.swift +18 -0
- package/swift/Sources/MacifyXPCService/XPCServiceHost.swift +75 -0
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./plugin/build/index.cjs");
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
//#region src/index.d.ts
|
|
2
|
+
type EventHandler = (data: any) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Client API for communicating with the XPC service.
|
|
5
|
+
*/
|
|
6
|
+
declare const macx: {
|
|
7
|
+
/**
|
|
8
|
+
* Call the XPC service with an RPC message.
|
|
9
|
+
* @param type - The message type (e.g. "ping", "processImage")
|
|
10
|
+
* @param payload - The payload object (will be JSON-serialized)
|
|
11
|
+
* @returns The response from the XPC service
|
|
12
|
+
*/
|
|
13
|
+
call<T = any>(type: string, payload?: Record<string, any>): Promise<T>;
|
|
14
|
+
/**
|
|
15
|
+
* Listen for events pushed from the XPC service, or connection state changes.
|
|
16
|
+
*
|
|
17
|
+
* XPC events: `macx.on("progress", (data) => { ... })`
|
|
18
|
+
* Connection state: `macx.on("connectionLost", () => { ... })`
|
|
19
|
+
* `macx.on("connectionRestored", () => { ... })`
|
|
20
|
+
*
|
|
21
|
+
* @param type - The event type to listen for
|
|
22
|
+
* @param handler - Callback invoked with the event payload
|
|
23
|
+
* @returns Unsubscribe function
|
|
24
|
+
*/
|
|
25
|
+
on(type: string, handler: EventHandler): () => void;
|
|
26
|
+
/**
|
|
27
|
+
* Remove all listeners for a specific event type.
|
|
28
|
+
* For fine-grained control, use the unsubscribe function returned by `on()`.
|
|
29
|
+
*/
|
|
30
|
+
off(type: string): void;
|
|
31
|
+
};
|
|
32
|
+
//#endregion
|
|
33
|
+
export { macx };
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Client that connects to the XPC service from the main app process.
|
|
4
|
+
/// Provides generic JSON RPC and receives events from the XPC service.
|
|
5
|
+
class MacifyServiceClient: NSObject, MacifyXPCEventReceiver {
|
|
6
|
+
static let shared = MacifyServiceClient()
|
|
7
|
+
|
|
8
|
+
private var connection: NSXPCConnection?
|
|
9
|
+
private var serviceName: String?
|
|
10
|
+
private var isConnected = false
|
|
11
|
+
|
|
12
|
+
/// Callback for events received from the XPC service.
|
|
13
|
+
/// Set by MacifyXPCModule to bridge events to JS.
|
|
14
|
+
var onEvent: ((String, [String: Any]) -> Void)?
|
|
15
|
+
|
|
16
|
+
/// Callback for connection state changes (lost/restored).
|
|
17
|
+
/// Set by MacifyXPCModule to bridge connection events to JS.
|
|
18
|
+
var onConnectionStateChange: ((ConnectionState) -> Void)?
|
|
19
|
+
|
|
20
|
+
enum ConnectionState {
|
|
21
|
+
case lost
|
|
22
|
+
case restored
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/// Configure the service name. Called once from MacifyXPCModule on init.
|
|
26
|
+
func configure(serviceName: String) {
|
|
27
|
+
self.serviceName = serviceName
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
private func getConnection() -> NSXPCConnection? {
|
|
31
|
+
if let existing = connection {
|
|
32
|
+
return existing
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
guard let name = serviceName else {
|
|
36
|
+
return nil
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// NSXPCConnection(serviceName:) is unavailable in Mac Catalyst Swift headers,
|
|
40
|
+
// but the API exists at runtime on macOS. We use an ObjC bridge to bypass
|
|
41
|
+
// the compile-time availability check.
|
|
42
|
+
let conn = XPCConnectionFactory.connection(withServiceName: name)
|
|
43
|
+
|
|
44
|
+
// Interface the XPC service exports (we call it)
|
|
45
|
+
conn.remoteObjectInterface = NSXPCInterface(
|
|
46
|
+
with: MacifyXPCProtocol.self
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Interface we export (XPC service calls us for events)
|
|
50
|
+
conn.exportedInterface = NSXPCInterface(
|
|
51
|
+
with: MacifyXPCEventReceiver.self
|
|
52
|
+
)
|
|
53
|
+
conn.exportedObject = self
|
|
54
|
+
|
|
55
|
+
conn.invalidationHandler = { [weak self] in
|
|
56
|
+
guard let self = self else { return }
|
|
57
|
+
self.connection = nil
|
|
58
|
+
if self.isConnected {
|
|
59
|
+
self.isConnected = false
|
|
60
|
+
self.onConnectionStateChange?(.lost)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
conn.interruptionHandler = { [weak self] in
|
|
65
|
+
guard let self = self else { return }
|
|
66
|
+
self.connection = nil
|
|
67
|
+
if self.isConnected {
|
|
68
|
+
self.isConnected = false
|
|
69
|
+
self.onConnectionStateChange?(.lost)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
conn.resume()
|
|
74
|
+
connection = conn
|
|
75
|
+
isConnected = true
|
|
76
|
+
|
|
77
|
+
return conn
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// Generic RPC call to the XPC service.
|
|
81
|
+
func call(type: String, payload: [String: Any]) async throws -> [String: Any] {
|
|
82
|
+
guard let conn = getConnection() else {
|
|
83
|
+
throw NSError(
|
|
84
|
+
domain: "MacifyXPC",
|
|
85
|
+
code: -3,
|
|
86
|
+
userInfo: [NSLocalizedDescriptionKey: "XPC connection was lost — service name not configured"]
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// If we're reconnecting after a lost connection, notify JS
|
|
91
|
+
if !isConnected {
|
|
92
|
+
isConnected = true
|
|
93
|
+
onConnectionStateChange?(.restored)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Encode payload to JSON Data
|
|
97
|
+
let payloadData: Data
|
|
98
|
+
if payload.isEmpty {
|
|
99
|
+
payloadData = Data()
|
|
100
|
+
} else {
|
|
101
|
+
payloadData = try JSONSerialization.data(withJSONObject: payload, options: [])
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return try await withCheckedThrowingContinuation { continuation in
|
|
105
|
+
let proxy = conn.remoteObjectProxyWithErrorHandler { error in
|
|
106
|
+
continuation.resume(throwing: error)
|
|
107
|
+
}
|
|
108
|
+
guard let service = proxy as? MacifyXPCProtocol else {
|
|
109
|
+
continuation.resume(
|
|
110
|
+
throwing: NSError(
|
|
111
|
+
domain: "MacifyXPC",
|
|
112
|
+
code: -1,
|
|
113
|
+
userInfo: [NSLocalizedDescriptionKey: "XPC connection was lost"]
|
|
114
|
+
)
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
service.call(type: type, payload: payloadData) { responseData, error in
|
|
120
|
+
if let error = error {
|
|
121
|
+
continuation.resume(throwing: error)
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
guard let data = responseData else {
|
|
125
|
+
continuation.resume(returning: [:])
|
|
126
|
+
return
|
|
127
|
+
}
|
|
128
|
+
do {
|
|
129
|
+
let result = try JSONSerialization.jsonObject(with: data, options: [])
|
|
130
|
+
continuation.resume(returning: (result as? [String: Any]) ?? [:])
|
|
131
|
+
} catch {
|
|
132
|
+
continuation.resume(throwing: error)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// MARK: - MacifyXPCEventReceiver
|
|
139
|
+
|
|
140
|
+
/// Called by the XPC service when it pushes an event to the app.
|
|
141
|
+
func handleEvent(type: String, payload: Data) {
|
|
142
|
+
let decoded: [String: Any]
|
|
143
|
+
if payload.isEmpty {
|
|
144
|
+
decoded = [:]
|
|
145
|
+
} else if let json = try? JSONSerialization.jsonObject(with: payload, options: []) as? [String: Any] {
|
|
146
|
+
decoded = json
|
|
147
|
+
} else {
|
|
148
|
+
decoded = [:]
|
|
149
|
+
}
|
|
150
|
+
onEvent?(type, decoded)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Pod::Spec.new do |s|
|
|
2
|
+
s.name = 'MacifyXPC'
|
|
3
|
+
s.version = '0.0.1'
|
|
4
|
+
s.summary = 'XPC service client for Mac Catalyst Expo apps'
|
|
5
|
+
s.description = 'Provides XPC communication between React Native and a macOS XPC service'
|
|
6
|
+
s.homepage = 'https://github.com/user/macify'
|
|
7
|
+
s.license = 'MIT'
|
|
8
|
+
s.author = 'Theo'
|
|
9
|
+
s.source = { git: '' }
|
|
10
|
+
|
|
11
|
+
s.platform = :ios, '15.1'
|
|
12
|
+
s.swift_version = '5.9'
|
|
13
|
+
s.static_framework = true
|
|
14
|
+
|
|
15
|
+
s.dependency 'ExpoModulesCore'
|
|
16
|
+
|
|
17
|
+
s.source_files = '**/*.{h,m,swift}'
|
|
18
|
+
end
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class MacifyXPCModule: Module {
|
|
4
|
+
private var hasListeners = false
|
|
5
|
+
|
|
6
|
+
public func definition() -> ModuleDefinition {
|
|
7
|
+
Name("MacifyXPC")
|
|
8
|
+
|
|
9
|
+
// Read the service name from Info.plist (set by the config plugin)
|
|
10
|
+
OnCreate {
|
|
11
|
+
if let serviceName = Bundle.main.infoDictionary?["MacifyXPCServiceName"] as? String {
|
|
12
|
+
MacifyServiceClient.shared.configure(serviceName: serviceName)
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Only bridge events to JS when there are active listeners
|
|
17
|
+
OnStartObserving {
|
|
18
|
+
self.hasListeners = true
|
|
19
|
+
|
|
20
|
+
// Bridge XPC events to JS
|
|
21
|
+
MacifyServiceClient.shared.onEvent = { [weak self] type, payload in
|
|
22
|
+
guard let self = self, self.hasListeners else { return }
|
|
23
|
+
if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
|
|
24
|
+
let jsonString = String(data: data, encoding: .utf8) {
|
|
25
|
+
self.sendEvent("onXPCEvent", [
|
|
26
|
+
"type": type,
|
|
27
|
+
"payload": jsonString,
|
|
28
|
+
])
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Bridge connection state changes to JS
|
|
33
|
+
MacifyServiceClient.shared.onConnectionStateChange = { [weak self] state in
|
|
34
|
+
guard let self = self, self.hasListeners else { return }
|
|
35
|
+
switch state {
|
|
36
|
+
case .lost:
|
|
37
|
+
self.sendEvent("onXPCConnectionState", [
|
|
38
|
+
"type": "connectionLost",
|
|
39
|
+
"payload": "",
|
|
40
|
+
])
|
|
41
|
+
case .restored:
|
|
42
|
+
self.sendEvent("onXPCConnectionState", [
|
|
43
|
+
"type": "connectionRestored",
|
|
44
|
+
"payload": "",
|
|
45
|
+
])
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
OnStopObserving {
|
|
51
|
+
self.hasListeners = false
|
|
52
|
+
MacifyServiceClient.shared.onEvent = nil
|
|
53
|
+
MacifyServiceClient.shared.onConnectionStateChange = nil
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
Events("onXPCEvent", "onXPCConnectionState")
|
|
57
|
+
|
|
58
|
+
/// Generic RPC call to the XPC service.
|
|
59
|
+
/// Takes a type string and a JSON-encoded payload string.
|
|
60
|
+
/// Returns a JSON-encoded response string.
|
|
61
|
+
AsyncFunction("call") { (type: String, payloadJSON: String) -> String in
|
|
62
|
+
// Decode the JSON payload string into a dictionary
|
|
63
|
+
let payload: [String: Any]
|
|
64
|
+
if payloadJSON.isEmpty || payloadJSON == "{}" {
|
|
65
|
+
payload = [:]
|
|
66
|
+
} else {
|
|
67
|
+
guard let data = payloadJSON.data(using: .utf8),
|
|
68
|
+
let decoded = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
|
69
|
+
throw NSError(
|
|
70
|
+
domain: "MacifyXPC",
|
|
71
|
+
code: -2,
|
|
72
|
+
userInfo: [NSLocalizedDescriptionKey: "Invalid JSON payload"]
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
payload = decoded
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let result = try await MacifyServiceClient.shared.call(type: type, payload: payload)
|
|
79
|
+
|
|
80
|
+
// Encode response back to JSON string for JS
|
|
81
|
+
let responseData = try JSONSerialization.data(withJSONObject: result, options: [])
|
|
82
|
+
return String(data: responseData, encoding: .utf8) ?? "{}"
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Wire protocol for RPC calls from the app to the XPC service.
|
|
4
|
+
/// This is the app-side copy — the XPC service has its own copy in the SPM package.
|
|
5
|
+
@objc protocol MacifyXPCProtocol {
|
|
6
|
+
func call(
|
|
7
|
+
type: String,
|
|
8
|
+
payload: Data,
|
|
9
|
+
withReply reply: @escaping (Data?, NSError?) -> Void
|
|
10
|
+
)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/// Reverse channel: the XPC service pushes events back to the app.
|
|
14
|
+
@objc protocol MacifyXPCEventReceiver {
|
|
15
|
+
func handleEvent(type: String, payload: Data)
|
|
16
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
/// Bridges NSXPCConnection(serviceName:) which is unavailable in Mac Catalyst
|
|
6
|
+
/// Swift headers. The API exists at runtime on macOS — it's only blocked at
|
|
7
|
+
/// compile time because Catalyst uses the iOS SDK surface.
|
|
8
|
+
@interface XPCConnectionFactory : NSObject
|
|
9
|
+
|
|
10
|
+
+ (NSXPCConnection *)connectionWithServiceName:(NSString *)serviceName;
|
|
11
|
+
|
|
12
|
+
@end
|
|
13
|
+
|
|
14
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#import "XPCConnectionFactory.h"
|
|
2
|
+
#import <objc/message.h>
|
|
3
|
+
|
|
4
|
+
@implementation XPCConnectionFactory
|
|
5
|
+
|
|
6
|
+
+ (NSXPCConnection *)connectionWithServiceName:(NSString *)serviceName {
|
|
7
|
+
// NSXPCConnection(serviceName:) is marked API_UNAVAILABLE(ios) which
|
|
8
|
+
// blocks compilation in Mac Catalyst even though it exists at runtime.
|
|
9
|
+
// Use objc_msgSend to bypass the compile-time availability check.
|
|
10
|
+
NSXPCConnection *conn = [NSXPCConnection alloc];
|
|
11
|
+
NSXPCConnection *(*sendTyped)(id, SEL, NSString *) =
|
|
12
|
+
(NSXPCConnection *(*)(id, SEL, NSString *))objc_msgSend;
|
|
13
|
+
conn = sendTyped(conn, NSSelectorFromString(@"initWithServiceName:"), serviceName);
|
|
14
|
+
return conn;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@end
|
package/package.json
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@macify/xpc",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.4",
|
|
4
4
|
"description": "XPC service support for Mac Catalyst Expo apps",
|
|
5
5
|
"main": "build/index.cjs",
|
|
6
6
|
"types": "build/index.d.cts",
|
|
7
|
+
"files": [
|
|
8
|
+
"build/",
|
|
9
|
+
"plugin/build/",
|
|
10
|
+
"ios/",
|
|
11
|
+
"swift/Package.swift",
|
|
12
|
+
"swift/Sources/",
|
|
13
|
+
"app.plugin.js",
|
|
14
|
+
"expo-module.config.json"
|
|
15
|
+
],
|
|
7
16
|
"license": "MIT",
|
|
8
17
|
"scripts": {
|
|
9
18
|
"build": "tsdown"
|
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __copyProps = (to, from, except, desc) => {
|
|
9
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
10
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
13
|
+
__defProp(to, key, {
|
|
14
|
+
get: ((k) => from[k]).bind(null, key),
|
|
15
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
23
|
+
value: mod,
|
|
24
|
+
enumerable: true
|
|
25
|
+
}) : target, mod));
|
|
26
|
+
|
|
27
|
+
//#endregion
|
|
28
|
+
let _expo_config_plugins = require("@expo/config-plugins");
|
|
29
|
+
let node_fs = require("node:fs");
|
|
30
|
+
node_fs = __toESM(node_fs);
|
|
31
|
+
let node_path = require("node:path");
|
|
32
|
+
node_path = __toESM(node_path);
|
|
33
|
+
|
|
34
|
+
//#region plugin/src/index.ts
|
|
35
|
+
const XPC_PRODUCT_TYPE = "com.apple.product-type.xpc-service";
|
|
36
|
+
const XPC_PRODUCT_FILE_TYPE = "wrapper.xpc-service";
|
|
37
|
+
const SPM_PRODUCT_NAME = "MacifyXPCService";
|
|
38
|
+
function getServiceName(config, props) {
|
|
39
|
+
if (props.serviceName) return props.serviceName;
|
|
40
|
+
return `${(config.name || "App").replace(/\s+/g, "")}Service`;
|
|
41
|
+
}
|
|
42
|
+
function getBundleId(config, serviceName) {
|
|
43
|
+
const appBundleId = config.ios?.bundleIdentifier;
|
|
44
|
+
if (!appBundleId) throw new Error("[xpc] No ios.bundleIdentifier set in Expo config. Required for XPC service.");
|
|
45
|
+
return `${appBundleId}.${serviceName}`;
|
|
46
|
+
}
|
|
47
|
+
/** Resolve the absolute path to @macify/xpc/swift/ */
|
|
48
|
+
function resolveSwiftPackagePath() {
|
|
49
|
+
const swiftDir = node_path.default.resolve(__dirname, "../../swift");
|
|
50
|
+
if (!node_fs.default.existsSync(swiftDir)) throw new Error(`[xpc] Could not find Swift package at ${swiftDir}. Is @macify/xpc installed?`);
|
|
51
|
+
return swiftDir;
|
|
52
|
+
}
|
|
53
|
+
/** Find the user's xpc-sources directory */
|
|
54
|
+
function findXPCSourcesDir(projectRoot) {
|
|
55
|
+
const xpcSourcesDir = node_path.default.join(projectRoot, "modules", "xpc-service", "xpc-sources");
|
|
56
|
+
if (!node_fs.default.existsSync(xpcSourcesDir)) throw new Error(`[xpc] Could not find XPC sources at ${xpcSourcesDir}. Run 'npx macx init' first.`);
|
|
57
|
+
return xpcSourcesDir;
|
|
58
|
+
}
|
|
59
|
+
const withXPCService = (config, props = {}) => {
|
|
60
|
+
const serviceName = getServiceName(config, props);
|
|
61
|
+
const bundleId = getBundleId(config, serviceName);
|
|
62
|
+
const macosTarget = props.macosDeploymentTarget || "13.0";
|
|
63
|
+
config = (0, _expo_config_plugins.withInfoPlist)(config, (config) => {
|
|
64
|
+
config.modResults.MacifyXPCServiceName = bundleId;
|
|
65
|
+
return config;
|
|
66
|
+
});
|
|
67
|
+
config = (0, _expo_config_plugins.withDangerousMod)(config, ["ios", async (config) => {
|
|
68
|
+
const iosDir = config.modRequest.platformProjectRoot;
|
|
69
|
+
const destDir = node_path.default.join(iosDir, serviceName);
|
|
70
|
+
const xpcSourcesDir = findXPCSourcesDir(config.modRequest.projectRoot);
|
|
71
|
+
if (node_fs.default.existsSync(destDir)) node_fs.default.rmSync(destDir, { recursive: true });
|
|
72
|
+
node_fs.default.mkdirSync(destDir, { recursive: true });
|
|
73
|
+
const files = node_fs.default.readdirSync(xpcSourcesDir);
|
|
74
|
+
for (const file of files) {
|
|
75
|
+
const src = node_path.default.join(xpcSourcesDir, file);
|
|
76
|
+
const dst = node_path.default.join(destDir, file);
|
|
77
|
+
if (node_fs.default.statSync(src).isFile()) node_fs.default.copyFileSync(src, dst);
|
|
78
|
+
}
|
|
79
|
+
console.log(`[xpc] Copied ${files.length} files to ios/${serviceName}/`);
|
|
80
|
+
return config;
|
|
81
|
+
}]);
|
|
82
|
+
config = (0, _expo_config_plugins.withXcodeProject)(config, async (config) => {
|
|
83
|
+
const project = config.modResults;
|
|
84
|
+
if (!config.ios?.appleTeamId) throw new Error("[xpc] No appleTeamId set in Expo config. Required for XPC service signing.");
|
|
85
|
+
const teamId = config.ios.appleTeamId;
|
|
86
|
+
if (project.pbxTargetByName(serviceName)) {
|
|
87
|
+
console.log(`[xpc] Target "${serviceName}" already exists, skipping.`);
|
|
88
|
+
return config;
|
|
89
|
+
}
|
|
90
|
+
const objects = project.hash.project.objects;
|
|
91
|
+
const swiftPackagePath = resolveSwiftPackagePath();
|
|
92
|
+
const iosDir = config.modRequest.platformProjectRoot;
|
|
93
|
+
const relativeSwiftPath = node_path.default.relative(iosDir, swiftPackagePath);
|
|
94
|
+
const spmRefUuid = project.generateUuid();
|
|
95
|
+
const spmRefSection = objects["XCLocalSwiftPackageReference"] || (objects["XCLocalSwiftPackageReference"] = {});
|
|
96
|
+
spmRefSection[spmRefUuid] = {
|
|
97
|
+
isa: "XCLocalSwiftPackageReference",
|
|
98
|
+
relativePath: `"${relativeSwiftPath}"`
|
|
99
|
+
};
|
|
100
|
+
spmRefSection[`${spmRefUuid}_comment`] = "XCLocalSwiftPackageReference \"MacifyXPCService\"";
|
|
101
|
+
const projectSection = project.pbxProjectSection();
|
|
102
|
+
for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) {
|
|
103
|
+
const proj = projectSection[key];
|
|
104
|
+
if (!proj.packageReferences) proj.packageReferences = [];
|
|
105
|
+
proj.packageReferences.push({
|
|
106
|
+
value: spmRefUuid,
|
|
107
|
+
comment: "XCLocalSwiftPackageReference \"MacifyXPCService\""
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const spmProductDepUuid = project.generateUuid();
|
|
111
|
+
const spmProductSection = objects["XCSwiftPackageProductDependency"] || (objects["XCSwiftPackageProductDependency"] = {});
|
|
112
|
+
spmProductSection[spmProductDepUuid] = {
|
|
113
|
+
isa: "XCSwiftPackageProductDependency",
|
|
114
|
+
package: spmRefUuid,
|
|
115
|
+
package_comment: "XCLocalSwiftPackageReference \"MacifyXPCService\"",
|
|
116
|
+
productName: SPM_PRODUCT_NAME
|
|
117
|
+
};
|
|
118
|
+
spmProductSection[`${spmProductDepUuid}_comment`] = SPM_PRODUCT_NAME;
|
|
119
|
+
const remoteProductDeps = [];
|
|
120
|
+
if (props.dependencies && props.dependencies.length > 0) {
|
|
121
|
+
const remoteRefSection = objects["XCRemoteSwiftPackageReference"] || (objects["XCRemoteSwiftPackageReference"] = {});
|
|
122
|
+
for (const dep of props.dependencies) {
|
|
123
|
+
if (!dep.version && !dep.exactVersion && !dep.branch) throw new Error(`[xpc] Remote dependency "${dep.url}" must specify one of: version, exactVersion, or branch.`);
|
|
124
|
+
let requirement;
|
|
125
|
+
if (dep.branch) requirement = {
|
|
126
|
+
kind: "branch",
|
|
127
|
+
branch: dep.branch
|
|
128
|
+
};
|
|
129
|
+
else if (dep.exactVersion) requirement = {
|
|
130
|
+
kind: "exactVersion",
|
|
131
|
+
version: dep.exactVersion
|
|
132
|
+
};
|
|
133
|
+
else requirement = {
|
|
134
|
+
kind: "upToNextMajorVersion",
|
|
135
|
+
minimumVersion: dep.version
|
|
136
|
+
};
|
|
137
|
+
const repoName = dep.url.replace(/\.git$/, "").split("/").pop() || dep.url;
|
|
138
|
+
const remoteRefUuid = project.generateUuid();
|
|
139
|
+
remoteRefSection[remoteRefUuid] = {
|
|
140
|
+
isa: "XCRemoteSwiftPackageReference",
|
|
141
|
+
repositoryURL: dep.url,
|
|
142
|
+
requirement
|
|
143
|
+
};
|
|
144
|
+
remoteRefSection[`${remoteRefUuid}_comment`] = `XCRemoteSwiftPackageReference "${repoName}"`;
|
|
145
|
+
for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) projectSection[key].packageReferences.push({
|
|
146
|
+
value: remoteRefUuid,
|
|
147
|
+
comment: `XCRemoteSwiftPackageReference "${repoName}"`
|
|
148
|
+
});
|
|
149
|
+
for (const productName of dep.products) {
|
|
150
|
+
const productDepUuid = project.generateUuid();
|
|
151
|
+
spmProductSection[productDepUuid] = {
|
|
152
|
+
isa: "XCSwiftPackageProductDependency",
|
|
153
|
+
package: remoteRefUuid,
|
|
154
|
+
package_comment: `XCRemoteSwiftPackageReference "${repoName}"`,
|
|
155
|
+
productName
|
|
156
|
+
};
|
|
157
|
+
spmProductSection[`${productDepUuid}_comment`] = productName;
|
|
158
|
+
remoteProductDeps.push({
|
|
159
|
+
productDepUuid,
|
|
160
|
+
productName
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
console.log(`[xpc] Added remote SPM dependency: ${repoName} (${dep.products.join(", ")})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
const sharedBuildSettings = {
|
|
167
|
+
SDKROOT: "macosx",
|
|
168
|
+
PRODUCT_NAME: `"$(TARGET_NAME)"`,
|
|
169
|
+
PRODUCT_BUNDLE_IDENTIFIER: `"${bundleId}"`,
|
|
170
|
+
INFOPLIST_FILE: `"${serviceName}/Info.plist"`,
|
|
171
|
+
SKIP_INSTALL: "YES",
|
|
172
|
+
MACOSX_DEPLOYMENT_TARGET: macosTarget,
|
|
173
|
+
DEVELOPMENT_TEAM: `"${teamId}"`,
|
|
174
|
+
CODE_SIGN_STYLE: "Automatic",
|
|
175
|
+
SWIFT_VERSION: "5.0",
|
|
176
|
+
COMBINE_HIDPI_IMAGES: "YES",
|
|
177
|
+
CLANG_ENABLE_MODULES: "YES",
|
|
178
|
+
SWIFT_OPTIMIZATION_LEVEL: "\"-Onone\""
|
|
179
|
+
};
|
|
180
|
+
const debugBuildSettings = {
|
|
181
|
+
...sharedBuildSettings,
|
|
182
|
+
DEBUG_INFORMATION_FORMAT: "\"dwarf\"",
|
|
183
|
+
GCC_OPTIMIZATION_LEVEL: "\"0\"",
|
|
184
|
+
SWIFT_ACTIVE_COMPILATION_CONDITIONS: "\"$(inherited) DEBUG\"",
|
|
185
|
+
MTL_ENABLE_DEBUG_INFO: "INCLUDE_SOURCE"
|
|
186
|
+
};
|
|
187
|
+
const releaseBuildSettings = {
|
|
188
|
+
...sharedBuildSettings,
|
|
189
|
+
DEBUG_INFORMATION_FORMAT: "\"dwarf-with-dsym\"",
|
|
190
|
+
SWIFT_OPTIMIZATION_LEVEL: "\"-O\"",
|
|
191
|
+
COPY_PHASE_STRIP: "YES",
|
|
192
|
+
ENABLE_NS_ASSERTIONS: "NO"
|
|
193
|
+
};
|
|
194
|
+
const configList = project.addXCConfigurationList([{
|
|
195
|
+
name: "Debug",
|
|
196
|
+
isa: "XCBuildConfiguration",
|
|
197
|
+
buildSettings: debugBuildSettings
|
|
198
|
+
}, {
|
|
199
|
+
name: "Release",
|
|
200
|
+
isa: "XCBuildConfiguration",
|
|
201
|
+
buildSettings: releaseBuildSettings
|
|
202
|
+
}], "Release", `Build configuration list for PBXNativeTarget "${serviceName}"`);
|
|
203
|
+
const productFileRefUuid = project.generateUuid();
|
|
204
|
+
objects.PBXFileReference[productFileRefUuid] = {
|
|
205
|
+
isa: "PBXFileReference",
|
|
206
|
+
explicitFileType: XPC_PRODUCT_FILE_TYPE,
|
|
207
|
+
includeInIndex: 0,
|
|
208
|
+
path: `${serviceName}.xpc`,
|
|
209
|
+
sourceTree: "BUILT_PRODUCTS_DIR",
|
|
210
|
+
name: `${serviceName}.xpc`
|
|
211
|
+
};
|
|
212
|
+
objects.PBXFileReference[`${productFileRefUuid}_comment`] = `${serviceName}.xpc`;
|
|
213
|
+
const productsGroupKey = project.findPBXGroupKey({ name: "Products" });
|
|
214
|
+
if (productsGroupKey) project.addToPbxGroup({
|
|
215
|
+
fileRef: productFileRefUuid,
|
|
216
|
+
basename: `${serviceName}.xpc`
|
|
217
|
+
}, productsGroupKey);
|
|
218
|
+
const xpcSourcesDir = findXPCSourcesDir(config.modRequest.projectRoot);
|
|
219
|
+
const swiftFiles = node_fs.default.readdirSync(xpcSourcesDir).filter((f) => node_fs.default.statSync(node_path.default.join(xpcSourcesDir, f)).isFile()).filter((f) => f.endsWith(".swift"));
|
|
220
|
+
const sourceFileRefs = [];
|
|
221
|
+
for (const fileName of swiftFiles) {
|
|
222
|
+
const fileRefUuid = project.generateUuid();
|
|
223
|
+
objects.PBXFileReference[fileRefUuid] = {
|
|
224
|
+
isa: "PBXFileReference",
|
|
225
|
+
lastKnownFileType: "sourcecode.swift",
|
|
226
|
+
path: fileName,
|
|
227
|
+
sourceTree: "\"<group>\"",
|
|
228
|
+
name: fileName
|
|
229
|
+
};
|
|
230
|
+
objects.PBXFileReference[`${fileRefUuid}_comment`] = fileName;
|
|
231
|
+
sourceFileRefs.push({
|
|
232
|
+
uuid: fileRefUuid,
|
|
233
|
+
name: fileName
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const infoPlistRefUuid = project.generateUuid();
|
|
237
|
+
objects.PBXFileReference[infoPlistRefUuid] = {
|
|
238
|
+
isa: "PBXFileReference",
|
|
239
|
+
lastKnownFileType: "text.plist.xml",
|
|
240
|
+
path: "Info.plist",
|
|
241
|
+
sourceTree: "\"<group>\"",
|
|
242
|
+
name: "Info.plist"
|
|
243
|
+
};
|
|
244
|
+
objects.PBXFileReference[`${infoPlistRefUuid}_comment`] = "Info.plist";
|
|
245
|
+
const groupUuid = project.generateUuid();
|
|
246
|
+
const groupChildren = [...sourceFileRefs.map((ref) => ({
|
|
247
|
+
value: ref.uuid,
|
|
248
|
+
comment: ref.name
|
|
249
|
+
})), {
|
|
250
|
+
value: infoPlistRefUuid,
|
|
251
|
+
comment: "Info.plist"
|
|
252
|
+
}];
|
|
253
|
+
objects.PBXGroup[groupUuid] = {
|
|
254
|
+
isa: "PBXGroup",
|
|
255
|
+
children: groupChildren,
|
|
256
|
+
name: serviceName,
|
|
257
|
+
path: serviceName,
|
|
258
|
+
sourceTree: "\"<group>\""
|
|
259
|
+
};
|
|
260
|
+
objects.PBXGroup[`${groupUuid}_comment`] = serviceName;
|
|
261
|
+
const firstProject = project.getFirstProject();
|
|
262
|
+
const mainGroupKey = firstProject.firstProject.mainGroup;
|
|
263
|
+
const mainGroup = project.getPBXGroupByKey(mainGroupKey);
|
|
264
|
+
if (mainGroup) mainGroup.children.push({
|
|
265
|
+
value: groupUuid,
|
|
266
|
+
comment: serviceName
|
|
267
|
+
});
|
|
268
|
+
const buildFileUuids = [];
|
|
269
|
+
for (const ref of sourceFileRefs) {
|
|
270
|
+
const buildFileUuid = project.generateUuid();
|
|
271
|
+
objects.PBXBuildFile[buildFileUuid] = {
|
|
272
|
+
isa: "PBXBuildFile",
|
|
273
|
+
fileRef: ref.uuid,
|
|
274
|
+
fileRef_comment: ref.name
|
|
275
|
+
};
|
|
276
|
+
objects.PBXBuildFile[`${buildFileUuid}_comment`] = `${ref.name} in Sources`;
|
|
277
|
+
buildFileUuids.push({
|
|
278
|
+
uuid: buildFileUuid,
|
|
279
|
+
name: ref.name
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
const sourcesBuildPhaseUuid = project.generateUuid();
|
|
283
|
+
objects.PBXSourcesBuildPhase = objects.PBXSourcesBuildPhase || {};
|
|
284
|
+
objects.PBXSourcesBuildPhase[sourcesBuildPhaseUuid] = {
|
|
285
|
+
isa: "PBXSourcesBuildPhase",
|
|
286
|
+
buildActionMask: 2147483647,
|
|
287
|
+
files: buildFileUuids.map((bf) => ({
|
|
288
|
+
value: bf.uuid,
|
|
289
|
+
comment: `${bf.name} in Sources`
|
|
290
|
+
})),
|
|
291
|
+
runOnlyForDeploymentPostprocessing: 0
|
|
292
|
+
};
|
|
293
|
+
objects.PBXSourcesBuildPhase[`${sourcesBuildPhaseUuid}_comment`] = "Sources";
|
|
294
|
+
const spmBuildFileUuid = project.generateUuid();
|
|
295
|
+
objects.PBXBuildFile[spmBuildFileUuid] = {
|
|
296
|
+
isa: "PBXBuildFile",
|
|
297
|
+
productRef: spmProductDepUuid,
|
|
298
|
+
productRef_comment: SPM_PRODUCT_NAME
|
|
299
|
+
};
|
|
300
|
+
objects.PBXBuildFile[`${spmBuildFileUuid}_comment`] = `${SPM_PRODUCT_NAME} in Frameworks`;
|
|
301
|
+
const frameworkFiles = [{
|
|
302
|
+
value: spmBuildFileUuid,
|
|
303
|
+
comment: `${SPM_PRODUCT_NAME} in Frameworks`
|
|
304
|
+
}];
|
|
305
|
+
for (const remoteDep of remoteProductDeps) {
|
|
306
|
+
const remoteBuildFileUuid = project.generateUuid();
|
|
307
|
+
objects.PBXBuildFile[remoteBuildFileUuid] = {
|
|
308
|
+
isa: "PBXBuildFile",
|
|
309
|
+
productRef: remoteDep.productDepUuid,
|
|
310
|
+
productRef_comment: remoteDep.productName
|
|
311
|
+
};
|
|
312
|
+
objects.PBXBuildFile[`${remoteBuildFileUuid}_comment`] = `${remoteDep.productName} in Frameworks`;
|
|
313
|
+
frameworkFiles.push({
|
|
314
|
+
value: remoteBuildFileUuid,
|
|
315
|
+
comment: `${remoteDep.productName} in Frameworks`
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
const frameworksBuildPhaseUuid = project.generateUuid();
|
|
319
|
+
objects.PBXFrameworksBuildPhase = objects.PBXFrameworksBuildPhase || {};
|
|
320
|
+
objects.PBXFrameworksBuildPhase[frameworksBuildPhaseUuid] = {
|
|
321
|
+
isa: "PBXFrameworksBuildPhase",
|
|
322
|
+
buildActionMask: 2147483647,
|
|
323
|
+
files: frameworkFiles,
|
|
324
|
+
runOnlyForDeploymentPostprocessing: 0
|
|
325
|
+
};
|
|
326
|
+
objects.PBXFrameworksBuildPhase[`${frameworksBuildPhaseUuid}_comment`] = "Frameworks";
|
|
327
|
+
const targetUuid = project.generateUuid();
|
|
328
|
+
const nativeTarget = {
|
|
329
|
+
isa: "PBXNativeTarget",
|
|
330
|
+
name: serviceName,
|
|
331
|
+
productName: serviceName,
|
|
332
|
+
productReference: productFileRefUuid,
|
|
333
|
+
productType: `"${XPC_PRODUCT_TYPE}"`,
|
|
334
|
+
buildConfigurationList: configList.uuid,
|
|
335
|
+
buildPhases: [{
|
|
336
|
+
value: sourcesBuildPhaseUuid,
|
|
337
|
+
comment: "Sources"
|
|
338
|
+
}, {
|
|
339
|
+
value: frameworksBuildPhaseUuid,
|
|
340
|
+
comment: "Frameworks"
|
|
341
|
+
}],
|
|
342
|
+
buildRules: [],
|
|
343
|
+
dependencies: []
|
|
344
|
+
};
|
|
345
|
+
nativeTarget.packageProductDependencies = [{
|
|
346
|
+
value: spmProductDepUuid,
|
|
347
|
+
comment: SPM_PRODUCT_NAME
|
|
348
|
+
}, ...remoteProductDeps.map((dep) => ({
|
|
349
|
+
value: dep.productDepUuid,
|
|
350
|
+
comment: dep.productName
|
|
351
|
+
}))];
|
|
352
|
+
objects.PBXNativeTarget[targetUuid] = nativeTarget;
|
|
353
|
+
objects.PBXNativeTarget[`${targetUuid}_comment`] = serviceName;
|
|
354
|
+
for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) projectSection[key].targets.push({
|
|
355
|
+
value: targetUuid,
|
|
356
|
+
comment: serviceName
|
|
357
|
+
});
|
|
358
|
+
const appTarget = project.getFirstTarget();
|
|
359
|
+
const appTargetUuid = appTarget.uuid;
|
|
360
|
+
const xpcCopyBuildFileUuid = project.generateUuid();
|
|
361
|
+
objects.PBXBuildFile[xpcCopyBuildFileUuid] = {
|
|
362
|
+
isa: "PBXBuildFile",
|
|
363
|
+
fileRef: productFileRefUuid,
|
|
364
|
+
fileRef_comment: `${serviceName}.xpc`,
|
|
365
|
+
settings: { ATTRIBUTES: ["RemoveHeadersOnCopy"] }
|
|
366
|
+
};
|
|
367
|
+
objects.PBXBuildFile[`${xpcCopyBuildFileUuid}_comment`] = `${serviceName}.xpc in Embed XPC Services`;
|
|
368
|
+
const copyPhaseUuid = project.generateUuid();
|
|
369
|
+
const copyPhaseSection = objects["PBXCopyFilesBuildPhase"] || (objects["PBXCopyFilesBuildPhase"] = {});
|
|
370
|
+
copyPhaseSection[copyPhaseUuid] = {
|
|
371
|
+
isa: "PBXCopyFilesBuildPhase",
|
|
372
|
+
buildActionMask: 2147483647,
|
|
373
|
+
dstPath: "\"$(CONTENTS_FOLDER_PATH)/XPCServices\"",
|
|
374
|
+
dstSubfolderSpec: 16,
|
|
375
|
+
files: [{
|
|
376
|
+
value: xpcCopyBuildFileUuid,
|
|
377
|
+
comment: `${serviceName}.xpc in Embed XPC Services`
|
|
378
|
+
}],
|
|
379
|
+
name: "\"Embed XPC Services\"",
|
|
380
|
+
runOnlyForDeploymentPostprocessing: 0
|
|
381
|
+
};
|
|
382
|
+
copyPhaseSection[`${copyPhaseUuid}_comment`] = "Embed XPC Services";
|
|
383
|
+
const appNativeTarget = objects.PBXNativeTarget[appTargetUuid] || appTarget.firstTarget;
|
|
384
|
+
if (appNativeTarget && "buildPhases" in appNativeTarget) appNativeTarget.buildPhases.push({
|
|
385
|
+
value: copyPhaseUuid,
|
|
386
|
+
comment: "Embed XPC Services"
|
|
387
|
+
});
|
|
388
|
+
const proxyUuid = project.generateUuid();
|
|
389
|
+
const proxySection = objects["PBXContainerItemProxy"] || (objects["PBXContainerItemProxy"] = {});
|
|
390
|
+
proxySection[proxyUuid] = {
|
|
391
|
+
isa: "PBXContainerItemProxy",
|
|
392
|
+
containerPortal: project.hash.project.rootObject,
|
|
393
|
+
containerPortal_comment: "Project object",
|
|
394
|
+
proxyType: 1,
|
|
395
|
+
remoteGlobalIDString: targetUuid,
|
|
396
|
+
remoteInfo: serviceName
|
|
397
|
+
};
|
|
398
|
+
proxySection[`${proxyUuid}_comment`] = "PBXContainerItemProxy";
|
|
399
|
+
const depUuid = project.generateUuid();
|
|
400
|
+
const depSection = objects["PBXTargetDependency"] || (objects["PBXTargetDependency"] = {});
|
|
401
|
+
depSection[depUuid] = {
|
|
402
|
+
isa: "PBXTargetDependency",
|
|
403
|
+
target: targetUuid,
|
|
404
|
+
target_comment: serviceName,
|
|
405
|
+
targetProxy: proxyUuid,
|
|
406
|
+
targetProxy_comment: "PBXContainerItemProxy"
|
|
407
|
+
};
|
|
408
|
+
depSection[`${depUuid}_comment`] = "PBXTargetDependency";
|
|
409
|
+
if (appNativeTarget && "dependencies" in appNativeTarget) appNativeTarget.dependencies.push({
|
|
410
|
+
value: depUuid,
|
|
411
|
+
comment: "PBXTargetDependency"
|
|
412
|
+
});
|
|
413
|
+
const projectObj = firstProject.firstProject;
|
|
414
|
+
if (!projectObj.attributes.TargetAttributes) projectObj.attributes.TargetAttributes = {};
|
|
415
|
+
projectObj.attributes.TargetAttributes[targetUuid] = {
|
|
416
|
+
CreatedOnToolsVersion: "15.0",
|
|
417
|
+
DevelopmentTeam: teamId
|
|
418
|
+
};
|
|
419
|
+
console.log(`[xpc] Added XPC target "${serviceName}" (${bundleId}) with SPM dependency`);
|
|
420
|
+
return config;
|
|
421
|
+
});
|
|
422
|
+
return config;
|
|
423
|
+
};
|
|
424
|
+
const pkg = {
|
|
425
|
+
name: "@macify/xpc",
|
|
426
|
+
version: "0.0.1"
|
|
427
|
+
};
|
|
428
|
+
var src_default = (0, _expo_config_plugins.createRunOncePlugin)(withXPCService, pkg.name, pkg.version);
|
|
429
|
+
|
|
430
|
+
//#endregion
|
|
431
|
+
module.exports = src_default;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ConfigPlugin } from "@expo/config-plugins";
|
|
2
|
+
|
|
3
|
+
//#region plugin/src/index.d.ts
|
|
4
|
+
type RemoteSPMDependency = {
|
|
5
|
+
url: string;
|
|
6
|
+
version?: string;
|
|
7
|
+
exactVersion?: string;
|
|
8
|
+
branch?: string;
|
|
9
|
+
products: string[];
|
|
10
|
+
};
|
|
11
|
+
type PluginProps = {
|
|
12
|
+
serviceName?: string;
|
|
13
|
+
macosDeploymentTarget?: string;
|
|
14
|
+
dependencies?: RemoteSPMDependency[];
|
|
15
|
+
};
|
|
16
|
+
declare const _default: ConfigPlugin<PluginProps>;
|
|
17
|
+
export = _default;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
import PackageDescription
|
|
3
|
+
|
|
4
|
+
let package = Package(
|
|
5
|
+
name: "MacifyXPCService",
|
|
6
|
+
platforms: [.macOS(.v13)],
|
|
7
|
+
products: [
|
|
8
|
+
.library(
|
|
9
|
+
name: "MacifyXPCService",
|
|
10
|
+
targets: ["MacifyXPCService"]
|
|
11
|
+
),
|
|
12
|
+
],
|
|
13
|
+
targets: [
|
|
14
|
+
.target(
|
|
15
|
+
name: "MacifyXPCService",
|
|
16
|
+
path: "Sources/MacifyXPCService"
|
|
17
|
+
),
|
|
18
|
+
]
|
|
19
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// The XPC service implementation that receives generic JSON RPC calls
|
|
4
|
+
/// and dispatches them to the user's `XPCMessageHandler`.
|
|
5
|
+
public class MacifyServiceImpl: NSObject, MacifyXPCProtocol {
|
|
6
|
+
private let handler: XPCMessageHandler
|
|
7
|
+
private let emitter: XPCEventEmitter
|
|
8
|
+
|
|
9
|
+
public init(handler: XPCMessageHandler, connection: NSXPCConnection) {
|
|
10
|
+
self.handler = handler
|
|
11
|
+
self.emitter = XPCEventEmitter(connection: connection)
|
|
12
|
+
super.init()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public func call(
|
|
16
|
+
type: String,
|
|
17
|
+
payload: Data,
|
|
18
|
+
withReply reply: @escaping (Data?, NSError?) -> Void
|
|
19
|
+
) {
|
|
20
|
+
Task {
|
|
21
|
+
do {
|
|
22
|
+
// Decode incoming JSON payload
|
|
23
|
+
let decoded: [String: Any]
|
|
24
|
+
if payload.isEmpty {
|
|
25
|
+
decoded = [:]
|
|
26
|
+
} else {
|
|
27
|
+
guard let json = try JSONSerialization.jsonObject(
|
|
28
|
+
with: payload, options: []
|
|
29
|
+
) as? [String: Any] else {
|
|
30
|
+
throw MacifyXPCError.invalidPayload
|
|
31
|
+
}
|
|
32
|
+
decoded = json
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Dispatch to user's handler
|
|
36
|
+
let result = try await handler.handle(
|
|
37
|
+
type: type, payload: decoded, emitter: emitter
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Encode response
|
|
41
|
+
let responseData = try JSONSerialization.data(
|
|
42
|
+
withJSONObject: result, options: []
|
|
43
|
+
)
|
|
44
|
+
reply(responseData, nil)
|
|
45
|
+
} catch {
|
|
46
|
+
let nsError = error as NSError
|
|
47
|
+
reply(nil, nsError)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Errors that can occur in the XPC service.
|
|
4
|
+
public enum MacifyXPCError: LocalizedError {
|
|
5
|
+
case unknownMethod(String)
|
|
6
|
+
case invalidPayload
|
|
7
|
+
case serializationFailed(String)
|
|
8
|
+
case connectionLost
|
|
9
|
+
|
|
10
|
+
public var errorDescription: String? {
|
|
11
|
+
switch self {
|
|
12
|
+
case .unknownMethod(let method):
|
|
13
|
+
return "Unknown XPC method: \(method)"
|
|
14
|
+
case .invalidPayload:
|
|
15
|
+
return "Invalid JSON payload"
|
|
16
|
+
case .serializationFailed(let detail):
|
|
17
|
+
return "Serialization failed: \(detail)"
|
|
18
|
+
case .connectionLost:
|
|
19
|
+
return "XPC connection was lost"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Wire protocol for RPC calls from the app to the XPC service.
|
|
4
|
+
/// Both the app-side pod and the XPC service SPM library compile their own
|
|
5
|
+
/// copy of this protocol definition.
|
|
6
|
+
@objc public protocol MacifyXPCProtocol {
|
|
7
|
+
/// Generic RPC: the app sends a message type and JSON-encoded payload,
|
|
8
|
+
/// the XPC service processes it and replies with JSON-encoded data or an error.
|
|
9
|
+
func call(
|
|
10
|
+
type: String,
|
|
11
|
+
payload: Data,
|
|
12
|
+
withReply reply: @escaping (Data?, NSError?) -> Void
|
|
13
|
+
)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Reverse channel: the XPC service pushes events back to the app.
|
|
17
|
+
/// The app sets its exported object conforming to this protocol on the
|
|
18
|
+
/// NSXPCConnection so the XPC service can call back into it.
|
|
19
|
+
@objc public protocol MacifyXPCEventReceiver {
|
|
20
|
+
func handleEvent(type: String, payload: Data)
|
|
21
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Allows the XPC service to push events back to the app.
|
|
4
|
+
/// The emitter holds a weak reference to the connection's event receiver proxy.
|
|
5
|
+
public class XPCEventEmitter {
|
|
6
|
+
private weak var connection: NSXPCConnection?
|
|
7
|
+
|
|
8
|
+
init(connection: NSXPCConnection) {
|
|
9
|
+
self.connection = connection
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/// Send an event to the app.
|
|
13
|
+
/// - Parameters:
|
|
14
|
+
/// - type: Event type string (e.g. "progress", "status")
|
|
15
|
+
/// - payload: Dictionary that will be JSON-encoded
|
|
16
|
+
public func emit(_ type: String, payload: [String: Any] = [:]) {
|
|
17
|
+
guard let connection = connection else { return }
|
|
18
|
+
|
|
19
|
+
do {
|
|
20
|
+
let data = try JSONSerialization.data(
|
|
21
|
+
withJSONObject: payload, options: []
|
|
22
|
+
)
|
|
23
|
+
let proxy = connection.remoteObjectProxy as? MacifyXPCEventReceiver
|
|
24
|
+
proxy?.handleEvent(type: type, payload: data)
|
|
25
|
+
} catch {
|
|
26
|
+
print("[MacifyXPCService] Failed to serialize event payload: \(error)")
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Protocol that users implement to handle XPC messages.
|
|
4
|
+
/// The user's `main.swift` creates a struct conforming to this and passes
|
|
5
|
+
/// it to `XPCServiceHost.start(handler:)`.
|
|
6
|
+
public protocol XPCMessageHandler {
|
|
7
|
+
/// Handle an incoming RPC call.
|
|
8
|
+
/// - Parameters:
|
|
9
|
+
/// - type: The message type string (e.g. "ping", "processImage")
|
|
10
|
+
/// - payload: Decoded JSON payload as a dictionary
|
|
11
|
+
/// - emitter: Used to push events back to the app
|
|
12
|
+
/// - Returns: A dictionary that will be JSON-encoded and sent back as the reply
|
|
13
|
+
func handle(
|
|
14
|
+
type: String,
|
|
15
|
+
payload: [String: Any],
|
|
16
|
+
emitter: XPCEventEmitter
|
|
17
|
+
) async throws -> [String: Any]
|
|
18
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Entry point for XPC services built with MacifyXPCService.
|
|
4
|
+
///
|
|
5
|
+
/// Usage in `main.swift`:
|
|
6
|
+
/// ```swift
|
|
7
|
+
/// import MacifyXPCService
|
|
8
|
+
///
|
|
9
|
+
/// struct MyHandler: XPCMessageHandler {
|
|
10
|
+
/// func handle(type: String, payload: [String: Any], emitter: XPCEventEmitter) async throws -> [String: Any] {
|
|
11
|
+
/// switch type {
|
|
12
|
+
/// case "ping":
|
|
13
|
+
/// return ["message": "pong"]
|
|
14
|
+
/// default:
|
|
15
|
+
/// throw MacifyXPCError.unknownMethod(type)
|
|
16
|
+
/// }
|
|
17
|
+
/// }
|
|
18
|
+
/// }
|
|
19
|
+
///
|
|
20
|
+
/// XPCServiceHost.start(handler: MyHandler())
|
|
21
|
+
/// ```
|
|
22
|
+
public enum XPCServiceHost {
|
|
23
|
+
/// Start the XPC service listener with the given message handler.
|
|
24
|
+
/// This function never returns — it runs the main run loop.
|
|
25
|
+
public static func start(handler: XPCMessageHandler) {
|
|
26
|
+
let delegate = ServiceDelegate(handler: handler)
|
|
27
|
+
let listener = NSXPCListener.service()
|
|
28
|
+
listener.delegate = delegate
|
|
29
|
+
|
|
30
|
+
// Keep a strong reference so the delegate isn't deallocated
|
|
31
|
+
_delegate = delegate
|
|
32
|
+
|
|
33
|
+
listener.resume()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Strong reference to prevent deallocation
|
|
37
|
+
private static var _delegate: ServiceDelegate?
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Internal delegate that vends `MacifyServiceImpl` instances to incoming connections.
|
|
41
|
+
class ServiceDelegate: NSObject, NSXPCListenerDelegate {
|
|
42
|
+
let handler: XPCMessageHandler
|
|
43
|
+
|
|
44
|
+
init(handler: XPCMessageHandler) {
|
|
45
|
+
self.handler = handler
|
|
46
|
+
super.init()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func listener(
|
|
50
|
+
_ listener: NSXPCListener,
|
|
51
|
+
shouldAcceptNewConnection connection: NSXPCConnection
|
|
52
|
+
) -> Bool {
|
|
53
|
+
// Set the interface the service exports (app calls us)
|
|
54
|
+
connection.exportedInterface = NSXPCInterface(
|
|
55
|
+
with: MacifyXPCProtocol.self
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
// Set the interface the app exports (we call back for events)
|
|
59
|
+
connection.remoteObjectInterface = NSXPCInterface(
|
|
60
|
+
with: MacifyXPCEventReceiver.self
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
// Vend the service implementation
|
|
64
|
+
connection.exportedObject = MacifyServiceImpl(
|
|
65
|
+
handler: handler, connection: connection
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
connection.invalidationHandler = {
|
|
69
|
+
// Connection cleaned up
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
connection.resume()
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
}
|