mobridge-mcp 0.0.1
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/README.md +155 -0
- package/dist/adb-baseline/act.mjs +1 -0
- package/dist/adb-baseline/capture-lib.mjs +2 -0
- package/dist/main.obf.cjs +2 -0
- package/dist/protocol/fixtures/01-login-views/expected.json +32 -0
- package/dist/protocol/fixtures/01-login-views/snapshot.json +20 -0
- package/dist/protocol/fixtures/02-alert-dialog/expected.json +22 -0
- package/dist/protocol/fixtures/02-alert-dialog/snapshot.json +20 -0
- package/dist/protocol/fixtures/03-recyclerview-list/expected.json +22 -0
- package/dist/protocol/fixtures/03-recyclerview-list/snapshot.json +19 -0
- package/dist/protocol/fixtures/04-ambiguous-ctas/expected.json +17 -0
- package/dist/protocol/fixtures/04-ambiguous-ctas/snapshot.json +21 -0
- package/dist/protocol/fixtures/05-compose-settings/expected.json +17 -0
- package/dist/protocol/fixtures/05-compose-settings/snapshot.json +19 -0
- package/dist/protocol/fixtures/06-canvas-game/expected.json +12 -0
- package/dist/protocol/fixtures/06-canvas-game/snapshot.json +16 -0
- package/dist/protocol/fixtures/07-search-results/expected.json +22 -0
- package/dist/protocol/fixtures/07-search-results/snapshot.json +19 -0
- package/dist/protocol/fixtures/README.md +34 -0
- package/dist/protocol/platform-capabilities.json +95 -0
- package/dist/protocol/schemas/action.schema.json +170 -0
- package/dist/protocol/schemas/common.schema.json +59 -0
- package/dist/protocol/schemas/diff.schema.json +74 -0
- package/dist/protocol/schemas/error.schema.json +34 -0
- package/dist/protocol/schemas/node-ref.schema.json +100 -0
- package/dist/protocol/schemas/query.schema.json +53 -0
- package/dist/protocol/schemas/screen-snapshot.schema.json +50 -0
- package/dist/protocol/schemas/semantic-node.schema.json +95 -0
- package/dist/protocol/schemas/tools.schema.json +215 -0
- package/dist/protocol/schemas/wait.schema.json +57 -0
- package/dist/protocol/synonyms.json +74 -0
- package/dist/protocol/validate/package-lock.json +71 -0
- package/dist/protocol/validate/package.json +13 -0
- package/dist/protocol/validate/query-engine.mjs +1 -0
- package/dist/protocol/validate/run-fixtures.mjs +6 -0
- package/dist/protocol/validate/schema-lint.mjs +2 -0
- package/dist/protocol/validate/validate-file.mjs +1 -0
- package/integrity.json +4 -0
- package/loader.cjs +85 -0
- package/package.json +40 -0
- package/resources/android/app-developer-debug.apk +0 -0
- package/resources/ios/MobileSemantic/Fixture/MobileSemanticFixtureApp.swift +50 -0
- package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/project.pbxproj +463 -0
- package/resources/ios/MobileSemantic/MobileSemantic.xcodeproj/xcshareddata/xcschemes/MobileSemanticUITests.xcscheme +97 -0
- package/resources/ios/MobileSemantic/UITests/BridgeServer.swift +216 -0
- package/resources/ios/MobileSemantic/UITests/XcuiSnapshot.swift +73 -0
- package/resources/ios/MobileSemantic/gen_project.rb +67 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import XCTest
|
|
3
|
+
import Darwin
|
|
4
|
+
|
|
5
|
+
// The XCTest Runner (WDA-style): a long-running UI test that binds a TCP socket on 127.0.0.1:8500 and
|
|
6
|
+
// services length-prefixed JSON-RPC requests by doing REAL XCUIApplication/XCUIElement work. TS stays
|
|
7
|
+
// the source of truth for NodeRef resolution + VerificationPolicy; this Runner only captures fresh
|
|
8
|
+
// snapshots, double-checks the candidate count, performs the native action, and returns before/after.
|
|
9
|
+
//
|
|
10
|
+
// Framing (frozen, matches PROTOCOL.md + bridgeTransport.ts): 4-byte big-endian length + UTF-8 JSON.
|
|
11
|
+
final class BridgeServer: XCTestCase {
|
|
12
|
+
|
|
13
|
+
private static let port: UInt16 = 8500
|
|
14
|
+
private let runnerInstanceId = UUID().uuidString
|
|
15
|
+
private var sessionToken = ""
|
|
16
|
+
private var app: XCUIApplication!
|
|
17
|
+
// The bundle id of the app currently under automation. nil = the default test-host target (fixture).
|
|
18
|
+
// app.open with a `bundleId` switches this to any already-installed app (XCUIApplication(bundleIdentifier:)),
|
|
19
|
+
// so CMS can read/act on App Store / system apps without re-signing them; only this Runner is signed.
|
|
20
|
+
private var targetBundleId: String? = nil
|
|
21
|
+
|
|
22
|
+
func testServe() throws {
|
|
23
|
+
continueAfterFailure = true
|
|
24
|
+
app = XCUIApplication()
|
|
25
|
+
app.launch()
|
|
26
|
+
|
|
27
|
+
let listenFd = try openListener(port: BridgeServer.port)
|
|
28
|
+
defer { close(listenFd) }
|
|
29
|
+
FileHandle.standardError.write("[runner] listening on 127.0.0.1:\(BridgeServer.port)\n".data(using: .utf8)!)
|
|
30
|
+
|
|
31
|
+
while true {
|
|
32
|
+
let fd = accept(listenFd, nil, nil)
|
|
33
|
+
if fd < 0 { continue }
|
|
34
|
+
var shutdown = false
|
|
35
|
+
while !shutdown, let req = readFrame(fd) {
|
|
36
|
+
let (resp, stop) = handle(req)
|
|
37
|
+
writeFrame(fd, resp)
|
|
38
|
+
shutdown = stop
|
|
39
|
+
}
|
|
40
|
+
close(fd)
|
|
41
|
+
if shutdown { break }
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// MARK: dispatch
|
|
46
|
+
|
|
47
|
+
private func handle(_ req: [String: Any]) -> ([String: Any], Bool) {
|
|
48
|
+
let requestId = req["requestId"] as? Int ?? 0
|
|
49
|
+
let method = req["method"] as? String ?? ""
|
|
50
|
+
let params = req["params"] as? [String: Any] ?? [:]
|
|
51
|
+
|
|
52
|
+
func ok(_ result: [String: Any]) -> ([String: Any], Bool) {
|
|
53
|
+
([ "requestId": requestId, "runnerInstanceId": runnerInstanceId, "sessionToken": sessionToken, "result": result ], false)
|
|
54
|
+
}
|
|
55
|
+
func err(_ code: String, _ msg: String, _ rec: [String] = []) -> ([String: Any], Bool) {
|
|
56
|
+
([ "requestId": requestId, "runnerInstanceId": runnerInstanceId, "sessionToken": sessionToken,
|
|
57
|
+
"error": ["error": code, "message": msg, "recovery": rec] ], false)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
switch method {
|
|
61
|
+
case "HELLO":
|
|
62
|
+
sessionToken = params["sessionToken"] as? String ?? UUID().uuidString
|
|
63
|
+
return ok(["protocolVersion": 1, "runnerInstanceId": runnerInstanceId, "sessionToken": sessionToken, "maxPayloadBytes": 16 * 1024 * 1024, "runnerState": "READY"])
|
|
64
|
+
case "PING":
|
|
65
|
+
return ok(["pong": true])
|
|
66
|
+
case "SHUTDOWN":
|
|
67
|
+
return ([ "requestId": requestId, "result": ["bye": true] ], true)
|
|
68
|
+
case "device.connect":
|
|
69
|
+
return ok(["deviceId": params["deviceId"] as? String ?? "ios-sim", "sessionId": runnerInstanceId, "capabilities": ["screen.get", "screen.find", "screen.act", "screen.wait", "screen.diff"]])
|
|
70
|
+
case "app.open":
|
|
71
|
+
return appOpen(params, ok: ok, err: err)
|
|
72
|
+
case "screen.get":
|
|
73
|
+
return ok(snapshot())
|
|
74
|
+
case "screen.act":
|
|
75
|
+
return screenAct(params, ok: ok, err: err)
|
|
76
|
+
case "system.action":
|
|
77
|
+
let a = params["action"] as? String ?? ""
|
|
78
|
+
// rc.2: DISPATCH + evidence only. HOME is business-level with no intrinsic predicate; the
|
|
79
|
+
// Runner never self-reports a verdict — TS is the sole verifier.
|
|
80
|
+
if a == "HOME" {
|
|
81
|
+
let before = snapshot()
|
|
82
|
+
XCUIDevice.shared.press(.home)
|
|
83
|
+
return ok(["dispatch": ["status": "DISPATCHED", "method": "GLOBAL_KEY"], "before": before, "after": snapshot()])
|
|
84
|
+
}
|
|
85
|
+
return err("ACTION_UNSUPPORTED", "system.action '\(a)' is not supported on iOS.", ["iOS BACK/RECENTS are not global actions."])
|
|
86
|
+
default:
|
|
87
|
+
return err("ACTION_UNSUPPORTED", "Unknown bridge method '\(method)'.", ["See PROTOCOL.md."])
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
private func isValidBundleId(_ b: String) -> Bool {
|
|
92
|
+
// reverse-DNS-ish: segments of [A-Za-z0-9-] joined by dots; guards against argv/format injection.
|
|
93
|
+
guard b.count <= 255 else { return false }
|
|
94
|
+
return b.range(of: "^[A-Za-z0-9][A-Za-z0-9-]*(\\.[A-Za-z0-9][A-Za-z0-9-]*)+$", options: .regularExpression) != nil
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private func snapshot() -> [String: Any] {
|
|
98
|
+
let bundle = targetBundleId ?? (Bundle.main.bundleIdentifier ?? "fixture")
|
|
99
|
+
if let snap = try? app.snapshot() {
|
|
100
|
+
return Xcui.toScreenSnapshot(snap, bundleId: bundle, deviceId: "ios-sim")
|
|
101
|
+
}
|
|
102
|
+
return ["schemaVersion": 1, "platform": "IOS", "nodes": [], "treeHash": "0", "packageName": bundle, "appId": bundle, "snapshotId": "s_empty", "deviceId": "ios-sim", "windowId": 0, "orientation": "portrait", "screenStateId": "ios:0", "compact": true, "timestamp": ISO8601DateFormatter().string(from: Date())]
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func appOpen(_ p: [String: Any], ok: ([String: Any]) -> ([String: Any], Bool), err: (String, String, [String]) -> ([String: Any], Bool)) -> ([String: Any], Bool) {
|
|
106
|
+
let before = snapshot()
|
|
107
|
+
let action = p["appAction"] as? String ?? "APP_OPEN"
|
|
108
|
+
// Optional target switch: drive any already-installed app by bundle id (no re-signing of the target).
|
|
109
|
+
if let bundleId = (p["bundleId"] as? String), !bundleId.isEmpty {
|
|
110
|
+
guard isValidBundleId(bundleId) else {
|
|
111
|
+
return err("ACTION_REJECTED", "Invalid bundleId '\(bundleId)'.", ["Pass a valid iOS bundle identifier, e.g. com.apple.Preferences."])
|
|
112
|
+
}
|
|
113
|
+
if bundleId != targetBundleId {
|
|
114
|
+
app = XCUIApplication(bundleIdentifier: bundleId)
|
|
115
|
+
targetBundleId = bundleId
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
switch action {
|
|
119
|
+
case "APP_OPEN", "APP_BRING_TO_FRONT": app.activate() // launches the target if not already running
|
|
120
|
+
case "APP_CLOSE_UI": XCUIDevice.shared.press(.home)
|
|
121
|
+
default: break
|
|
122
|
+
}
|
|
123
|
+
// rc.2: DISPATCH + before/after only. TS applies the intrinsic PACKAGE_CHANGED postcondition.
|
|
124
|
+
return ok(["dispatch": ["status": "DISPATCHED", "method": "ACCESSIBILITY_ACTION"], "before": before, "after": snapshot()])
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// TS has already resolved the NodeRef; the Runner re-queries by the resolved locator, double-checks
|
|
128
|
+
// the count (defence in depth), performs the verb, and returns before/after snapshots.
|
|
129
|
+
private func screenAct(_ p: [String: Any], ok: ([String: Any]) -> ([String: Any], Bool), err: (String, String, [String]) -> ([String: Any], Bool)) -> ([String: Any], Bool) {
|
|
130
|
+
let before = snapshot()
|
|
131
|
+
let verb = p["verb"] as? String ?? "CLICK"
|
|
132
|
+
let query: XCUIElementQuery
|
|
133
|
+
if let aid = p["automationId"] as? String, !aid.isEmpty {
|
|
134
|
+
query = app.descendants(matching: .any).matching(identifier: aid)
|
|
135
|
+
} else if let label = p["label"] as? String, !label.isEmpty {
|
|
136
|
+
query = app.descendants(matching: .any).matching(NSPredicate(format: "label == %@", label))
|
|
137
|
+
} else {
|
|
138
|
+
return err("NODE_NOT_FOUND", "screen.act needs automationId or label.", ["Resolve a NodeRef first."])
|
|
139
|
+
}
|
|
140
|
+
let count = query.count
|
|
141
|
+
if count == 0 { return err("NODE_NOT_FOUND", "No element matched on the live screen.", ["Re-read screen.get."]) }
|
|
142
|
+
if count > 1 { return err("AMBIGUOUS_MATCH", "\(count) elements matched; refusing to guess.", ["Use a stronger locator (automationId)."]) }
|
|
143
|
+
let el = query.element
|
|
144
|
+
guard el.waitForExistence(timeout: 3) else { return err("NODE_NOT_FOUND", "Element did not become resolvable.", []) }
|
|
145
|
+
switch verb {
|
|
146
|
+
case "SET_TEXT":
|
|
147
|
+
el.tap()
|
|
148
|
+
// Clear any existing content first so SET_TEXT is a replace, not an append.
|
|
149
|
+
if let cur = el.value as? String, !cur.isEmpty {
|
|
150
|
+
el.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: cur.count))
|
|
151
|
+
}
|
|
152
|
+
el.typeText(p["value"] as? String ?? "")
|
|
153
|
+
case "CLEAR_TEXT":
|
|
154
|
+
el.tap()
|
|
155
|
+
if let cur = el.value as? String, !cur.isEmpty {
|
|
156
|
+
el.typeText(String(repeating: XCUIKeyboardKey.delete.rawValue, count: cur.count))
|
|
157
|
+
}
|
|
158
|
+
default:
|
|
159
|
+
el.tap()
|
|
160
|
+
}
|
|
161
|
+
// Let the UI settle (SwiftUI re-render) before the after-snapshot, mirroring Android waitStable.
|
|
162
|
+
Thread.sleep(forTimeInterval: 0.4)
|
|
163
|
+
return ok(["before": before, "after": snapshot(), "evidence": ["screenChanged": true]])
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// MARK: sockets (length-prefixed framing)
|
|
167
|
+
|
|
168
|
+
private func openListener(port: UInt16) throws -> Int32 {
|
|
169
|
+
let fd = socket(AF_INET, SOCK_STREAM, 0)
|
|
170
|
+
guard fd >= 0 else { throw NSError(domain: "runner", code: 1) }
|
|
171
|
+
var yes: Int32 = 1
|
|
172
|
+
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &yes, socklen_t(MemoryLayout<Int32>.size))
|
|
173
|
+
var addr = sockaddr_in()
|
|
174
|
+
addr.sin_family = sa_family_t(AF_INET)
|
|
175
|
+
addr.sin_port = port.bigEndian
|
|
176
|
+
addr.sin_addr.s_addr = inet_addr("127.0.0.1")
|
|
177
|
+
let bound = withUnsafePointer(to: &addr) { $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { bind(fd, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) } }
|
|
178
|
+
guard bound == 0 else { close(fd); throw NSError(domain: "runner", code: 2) }
|
|
179
|
+
guard listen(fd, 1) == 0 else { close(fd); throw NSError(domain: "runner", code: 3) }
|
|
180
|
+
return fd
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private func readN(_ fd: Int32, _ n: Int) -> Data? {
|
|
184
|
+
var out = Data(); out.reserveCapacity(n)
|
|
185
|
+
var buf = [UInt8](repeating: 0, count: 65536)
|
|
186
|
+
while out.count < n {
|
|
187
|
+
let want = min(n - out.count, buf.count)
|
|
188
|
+
let r = recv(fd, &buf, want, 0)
|
|
189
|
+
if r <= 0 { return nil }
|
|
190
|
+
out.append(contentsOf: buf[0..<r])
|
|
191
|
+
}
|
|
192
|
+
return out
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private func readFrame(_ fd: Int32) -> [String: Any]? {
|
|
196
|
+
guard let lenData = readN(fd, 4) else { return nil }
|
|
197
|
+
let len = lenData.withUnsafeBytes { $0.load(as: UInt32.self).bigEndian }
|
|
198
|
+
guard len > 0, len <= 16 * 1024 * 1024, let body = readN(fd, Int(len)) else { return nil }
|
|
199
|
+
return (try? JSONSerialization.jsonObject(with: body)) as? [String: Any]
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
private func writeFrame(_ fd: Int32, _ obj: [String: Any]) {
|
|
203
|
+
guard let body = try? JSONSerialization.data(withJSONObject: obj) else { return }
|
|
204
|
+
var len = UInt32(body.count).bigEndian
|
|
205
|
+
var frame = Data(bytes: &len, count: 4); frame.append(body)
|
|
206
|
+
frame.withUnsafeBytes { raw in
|
|
207
|
+
var off = 0
|
|
208
|
+
let base = raw.bindMemory(to: UInt8.self).baseAddress!
|
|
209
|
+
while off < frame.count {
|
|
210
|
+
let w = send(fd, base + off, frame.count - off, 0)
|
|
211
|
+
if w <= 0 { break }
|
|
212
|
+
off += w
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import XCTest
|
|
3
|
+
|
|
4
|
+
// Real XCUIElementSnapshot -> platform-neutral ScreenSnapshot (protocol/schemas). This is the
|
|
5
|
+
// production mapper (the Sources/MsmSupervisor/SnapshotModel.swift copy is the no-XCTest stand-in).
|
|
6
|
+
enum Xcui {
|
|
7
|
+
|
|
8
|
+
static func role(for type: XCUIElement.ElementType) -> String {
|
|
9
|
+
switch type {
|
|
10
|
+
case .button: return "BUTTON"
|
|
11
|
+
case .staticText: return "TEXT"
|
|
12
|
+
case .textField, .secureTextField, .searchField: return "INPUT"
|
|
13
|
+
case .image: return "IMAGE"
|
|
14
|
+
case .checkBox: return "CHECKBOX"
|
|
15
|
+
case .radioButton: return "RADIO"
|
|
16
|
+
case .switch, .toggle: return "SWITCH"
|
|
17
|
+
case .tab, .tabBar: return "TAB"
|
|
18
|
+
case .link: return "LINK"
|
|
19
|
+
case .slider: return "SLIDER"
|
|
20
|
+
case .progressIndicator: return "PROGRESS"
|
|
21
|
+
case .table, .collectionView: return "LIST"
|
|
22
|
+
case .cell: return "LIST_ITEM"
|
|
23
|
+
case .webView: return "WEB_VIEW"
|
|
24
|
+
case .window, .application: return "SCREEN"
|
|
25
|
+
case .sheet, .alert, .dialog: return "DIALOG"
|
|
26
|
+
default: return "UNKNOWN"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/// FNV-1a over the ordered node signatures — parity with the Android collector's treeHash.
|
|
31
|
+
static func treeHash(_ nodes: [[String: Any]]) -> String {
|
|
32
|
+
let sig = nodes.map { n in
|
|
33
|
+
[n["role"] as? String ?? "", n["label"] as? String ?? "", n["value"] as? String ?? "",
|
|
34
|
+
n["automationId"] as? String ?? "", (n["bounds"] as? [Int])?.map(String.init).joined(separator: ",") ?? ""].joined(separator: "|")
|
|
35
|
+
}.joined(separator: "\n")
|
|
36
|
+
var h: UInt32 = 2166136261
|
|
37
|
+
for b in sig.utf8 { h = (h ^ UInt32(b)) &* 16777619 }
|
|
38
|
+
return String(format: "%08x", h)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static func toScreenSnapshot(_ root: XCUIElementSnapshot, bundleId: String, deviceId: String) -> [String: Any] {
|
|
42
|
+
var nodes: [[String: Any]] = []
|
|
43
|
+
var counter = 0
|
|
44
|
+
func walk(_ el: XCUIElementSnapshot, parentId: String?) {
|
|
45
|
+
let id = "n\(counter)"; counter += 1
|
|
46
|
+
let f = el.frame
|
|
47
|
+
var node: [String: Any] = [
|
|
48
|
+
"nodeId": id,
|
|
49
|
+
"role": role(for: el.elementType),
|
|
50
|
+
"nativeType": "XCUIElementType\(el.elementType.rawValue)",
|
|
51
|
+
]
|
|
52
|
+
if let p = parentId { node["parentId"] = p } else { node["parentId"] = NSNull() }
|
|
53
|
+
let ident = el.identifier
|
|
54
|
+
if !ident.isEmpty { node["automationId"] = ident; node["resourceId"] = ident }
|
|
55
|
+
if !el.label.isEmpty { node["label"] = el.label }
|
|
56
|
+
if let v = el.value as? String, !v.isEmpty { node["value"] = v }
|
|
57
|
+
node["bounds"] = [Int(f.minX), Int(f.minY), Int(f.maxX), Int(f.maxY)]
|
|
58
|
+
let isSecure = el.elementType == .secureTextField
|
|
59
|
+
node["state"] = ["enabled": el.isEnabled, "selected": el.isSelected, "password": isSecure]
|
|
60
|
+
nodes.append(node)
|
|
61
|
+
for c in el.children { walk(c, parentId: id) }
|
|
62
|
+
}
|
|
63
|
+
walk(root, parentId: nil)
|
|
64
|
+
let th = treeHash(nodes)
|
|
65
|
+
return [
|
|
66
|
+
"schemaVersion": 1, "platform": "IOS", "deviceId": deviceId,
|
|
67
|
+
"snapshotId": "s_\(UUID().uuidString.prefix(8))",
|
|
68
|
+
"timestamp": ISO8601DateFormatter().string(from: Date()),
|
|
69
|
+
"packageName": bundleId, "appId": bundleId, "windowId": 0, "orientation": "portrait",
|
|
70
|
+
"screenStateId": "ios:\(th.prefix(4))", "compact": true, "nodes": nodes, "treeHash": th,
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# Generate MobileSemantic.xcodeproj (fixture app + UI-test bundle) via the xcodeproj gem — no Xcode
|
|
3
|
+
# GUI, no xcodegen. Simulator build, no signing. Run: `ruby gen_project.rb`.
|
|
4
|
+
require 'xcodeproj'
|
|
5
|
+
|
|
6
|
+
here = File.dirname(File.expand_path(__FILE__))
|
|
7
|
+
proj_path = File.join(here, 'MobileSemantic.xcodeproj')
|
|
8
|
+
proj = Xcodeproj::Project.new(proj_path)
|
|
9
|
+
|
|
10
|
+
DEPLOY = '14.0'
|
|
11
|
+
# Physical-device signing (M5). Simulator builds ignore this; device builds use automatic signing
|
|
12
|
+
# with this development team. Override with MSM_DEV_TEAM to use a different Apple account.
|
|
13
|
+
DEV_TEAM = ENV['MSM_DEV_TEAM'] || 'A2KNU2X4F7'
|
|
14
|
+
|
|
15
|
+
# --- fixture app target ------------------------------------------------------
|
|
16
|
+
app = proj.new_target(:application, 'MobileSemanticFixture', :ios, DEPLOY)
|
|
17
|
+
app_group = proj.new_group('Fixture', 'Fixture')
|
|
18
|
+
Dir.glob(File.join(here, 'Fixture', '*.swift')).sort.each do |f|
|
|
19
|
+
ref = app_group.new_file(f)
|
|
20
|
+
app.source_build_phase.add_file_reference(ref)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# --- UI-test bundle target ---------------------------------------------------
|
|
24
|
+
uitest = proj.new_target(:ui_test_bundle, 'MobileSemanticUITests', :ios, DEPLOY)
|
|
25
|
+
ui_group = proj.new_group('UITests', 'UITests')
|
|
26
|
+
Dir.glob(File.join(here, 'UITests', '*.swift')).sort.each do |f|
|
|
27
|
+
ref = ui_group.new_file(f)
|
|
28
|
+
uitest.source_build_phase.add_file_reference(ref)
|
|
29
|
+
end
|
|
30
|
+
uitest.add_dependency(app)
|
|
31
|
+
|
|
32
|
+
# --- build settings ----------------------------------------------------------
|
|
33
|
+
common = {
|
|
34
|
+
'IPHONEOS_DEPLOYMENT_TARGET' => DEPLOY,
|
|
35
|
+
'SWIFT_VERSION' => '5.0',
|
|
36
|
+
# Automatic signing so a device build works; the simulator SDK skips signing regardless.
|
|
37
|
+
'CODE_SIGN_STYLE' => 'Automatic',
|
|
38
|
+
'DEVELOPMENT_TEAM' => DEV_TEAM,
|
|
39
|
+
'CODE_SIGN_IDENTITY[sdk=iphoneos*]' => 'Apple Development',
|
|
40
|
+
'CODE_SIGNING_ALLOWED[sdk=iphonesimulator*]' => 'NO',
|
|
41
|
+
'GENERATE_INFOPLIST_FILE' => 'YES',
|
|
42
|
+
'CURRENT_PROJECT_VERSION' => '1',
|
|
43
|
+
'MARKETING_VERSION' => '1.0',
|
|
44
|
+
'ALWAYS_SEARCH_USER_PATHS' => 'NO',
|
|
45
|
+
}
|
|
46
|
+
app.build_configurations.each do |c|
|
|
47
|
+
c.build_settings.merge!(common)
|
|
48
|
+
c.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.msm.MobileSemanticFixture'
|
|
49
|
+
c.build_settings['INFOPLIST_KEY_UILaunchScreen_Generation'] = 'YES'
|
|
50
|
+
c.build_settings['INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents'] = 'YES'
|
|
51
|
+
end
|
|
52
|
+
uitest.build_configurations.each do |c|
|
|
53
|
+
c.build_settings.merge!(common)
|
|
54
|
+
c.build_settings['PRODUCT_BUNDLE_IDENTIFIER'] = 'com.msm.MobileSemanticUITests'
|
|
55
|
+
c.build_settings['TEST_TARGET_NAME'] = 'MobileSemanticFixture'
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# --- shared scheme so `xcodebuild -scheme MobileSemanticUITests test` works ---
|
|
59
|
+
proj.save
|
|
60
|
+
|
|
61
|
+
scheme = Xcodeproj::XCScheme.new
|
|
62
|
+
scheme.add_build_target(uitest)
|
|
63
|
+
scheme.add_test_target(uitest)
|
|
64
|
+
scheme.set_launch_target(app)
|
|
65
|
+
scheme.save_as(proj_path, 'MobileSemanticUITests', true)
|
|
66
|
+
|
|
67
|
+
puts "generated #{proj_path}"
|