catalyst-core 1.1.0 → 1.2.0-canary.2
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/bin/catalyst.js +54 -6
- package/changelog.md +27 -1
- package/dist/errors/registry.js +306 -18
- package/dist/errors-index.json +273 -70
- package/dist/native/androidProject/app/build.gradle.kts +53 -79
- package/dist/native/androidProject/app/src/main/AndroidManifest.xml +1 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/CatalystConstants.kt +3 -1
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/MainActivity.kt +32 -4
- package/dist/native/androidProject/app/src/main/java/io/yourname/androidproject/utils/KeyboardUtil.kt +5 -1
- package/dist/native/androidProject/app/src/test/java/io/yourname/androidproject/ConfigMappingTest.kt +25 -0
- package/dist/native/androidSetup.js +76 -170
- package/dist/native/androidToolchain.js +642 -0
- package/dist/native/appIcon.js +100 -0
- package/dist/native/buildAndroid/assets.js +29 -5
- package/dist/native/buildAndroid/build.js +110 -223
- package/dist/native/buildAndroid/config.js +57 -65
- package/dist/native/buildAndroid/devices.js +176 -0
- package/dist/native/buildAndroid/index.js +271 -46
- package/dist/native/buildAndroid/output.js +43 -0
- package/dist/native/buildAndroid/signing.js +456 -0
- package/dist/native/buildAppAndroid.js +27 -7
- package/dist/native/buildAppIos.js +139 -20
- package/dist/native/buildIos/assets.js +76 -4
- package/dist/native/buildIos/build.js +31 -181
- package/dist/native/buildIos/config.js +126 -11
- package/dist/native/buildIos/index.js +354 -34
- package/dist/native/buildIos/localhostCert.js +87 -0
- package/dist/native/buildIos/output.js +28 -0
- package/dist/native/buildIos/signing.js +596 -0
- package/dist/native/credentials.js +309 -0
- package/dist/native/exportCredentials.js +51 -0
- package/dist/native/iosToolchain.js +583 -0
- package/dist/native/iosnativeWebView/Configurations/Shared.xcconfig +1 -3
- package/dist/native/iosnativeWebView/iosnativeWebView/Info-Release.plist +2 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/PrivacyInfo.xcprivacy +40 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/iosnativeWebView.entitlements +0 -2
- package/dist/native/iosnativeWebView/iosnativeWebView.xcodeproj/project.pbxproj +4 -4
- package/dist/native/localState.js +148 -0
- package/dist/native/nativeConfig.js +621 -0
- package/dist/native/releaseOutput.js +53 -0
- package/dist/native/renameAndroidProject.js +154 -369
- package/dist/native/setupEmulatorIos.js +58 -275
- package/dist/native/setupServer.js +520 -124
- package/dist/native/utils.js +418 -115
- package/dist/vite/resolveDevServerConfig.js +21 -0
- package/mcp_v2/knowledge-base.json +13 -11
- package/mcp_v2/test/config.test.ts +40 -0
- package/mcp_v2/tools/build.js +80 -104
- package/mcp_v2/tools/config.js +181 -174
- package/package.json +1 -1
- package/test/cli/nativeFlags.test.ts +97 -0
- package/test/native/androidDevices.test.ts +230 -0
- package/test/native/androidElfAlignment.test.ts +148 -0
- package/test/native/androidSigning.test.ts +646 -0
- package/test/native/androidToolchain.test.ts +766 -0
- package/test/native/appIcon.test.ts +124 -0
- package/test/native/credentials.test.ts +241 -0
- package/test/native/interaction.test.ts +179 -0
- package/test/native/iosConfigPhase.test.ts +611 -0
- package/test/native/iosSigning.test.ts +1060 -0
- package/test/native/iosToolchain.test.ts +821 -0
- package/test/native/localState.test.ts +80 -0
- package/test/native/nativeConfig.test.ts +602 -0
- package/test/native/setupServer.test.ts +529 -0
- package/test/native/utils.test.ts +102 -127
- package/test/vite/resolveDevServerConfig.test.js +25 -0
- package/dist/native/androidProject/keystore/Untitled +0 -0
- package/dist/native/androidProject/keystore/release-key.jks +0 -0
- package/dist/native/iosnativeWebView/iosnativeWebView/localhost.p12 +0 -0
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.DEFAULT_SIMULATOR_NAME = void 0;
|
|
7
|
+
exports.assertFullXcode = assertFullXcode;
|
|
8
|
+
exports.availableIosRuntimes = availableIosRuntimes;
|
|
9
|
+
exports.chooseIosTarget = chooseIosTarget;
|
|
10
|
+
exports.devHostFor = devHostFor;
|
|
11
|
+
exports.ensureIosRuntime = ensureIosRuntime;
|
|
12
|
+
exports.ensureSimulator = ensureSimulator;
|
|
13
|
+
exports.ensureXcodeReady = ensureXcodeReady;
|
|
14
|
+
exports.findSimulator = findSimulator;
|
|
15
|
+
exports.listConnectedIphones = listConnectedIphones;
|
|
16
|
+
exports.newestIphoneDeviceType = newestIphoneDeviceType;
|
|
17
|
+
exports.newestIphoneSimulator = newestIphoneSimulator;
|
|
18
|
+
exports.parseDevicectlDevices = parseDevicectlDevices;
|
|
19
|
+
exports.pendingXcodeSetup = pendingXcodeSetup;
|
|
20
|
+
exports.prepareIosDebugTarget = prepareIosDebugTarget;
|
|
21
|
+
exports.probe = probe;
|
|
22
|
+
exports.runDevicectlList = runDevicectlList;
|
|
23
|
+
exports.simctlJson = simctlJson;
|
|
24
|
+
exports.simulatorsFor = simulatorsFor;
|
|
25
|
+
var _child_process = require("child_process");
|
|
26
|
+
var _fs = _interopRequireDefault(require("fs"));
|
|
27
|
+
var _os = _interopRequireDefault(require("os"));
|
|
28
|
+
var _path = _interopRequireDefault(require("path"));
|
|
29
|
+
var _utils = require("./utils.js");
|
|
30
|
+
var _setupServer = require("./setupServer.js");
|
|
31
|
+
var _localState = require("./localState.js");
|
|
32
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
33
|
+
// The iOS toolchain checks `buildApp:ios` runs before it builds, and
|
|
34
|
+
// `setupEmulator:ios` runs on its own: full Xcode with its licence and
|
|
35
|
+
// first-launch components, an iOS simulator runtime, a simulator, and the
|
|
36
|
+
// choice between that simulator and a connected iPhone. Installs ask first
|
|
37
|
+
// (askConsent "install", answered by --yes), the licence asks separately
|
|
38
|
+
// ("licence", answered by --accept-licenses). Nothing here writes config.json.
|
|
39
|
+
|
|
40
|
+
const DEFAULT_SIMULATOR_NAME = exports.DEFAULT_SIMULATOR_NAME = "Catalyst iPhone";
|
|
41
|
+
|
|
42
|
+
/** Exit status and output of a probe; never throws. */
|
|
43
|
+
function probe(command, args, {
|
|
44
|
+
spawn = _child_process.spawnSync
|
|
45
|
+
} = {}) {
|
|
46
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Fixed Xcode probe commands with argv arrays.
|
|
47
|
+
const result = spawn(command, args, {
|
|
48
|
+
encoding: "utf8",
|
|
49
|
+
timeout: 60000
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
status: result.error ? null : result.status,
|
|
53
|
+
output: `${result.stdout || ""}${result.stderr || ""}`.trim()
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
function simctlJson(args, {
|
|
57
|
+
exec = _child_process.execFileSync
|
|
58
|
+
} = {}) {
|
|
59
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Fixed simctl queries with argv arrays.
|
|
60
|
+
return JSON.parse(exec("xcrun", ["simctl", ...args, "-j"], {
|
|
61
|
+
encoding: "utf8"
|
|
62
|
+
}));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The active developer directory must be inside an Xcode app bundle. The
|
|
67
|
+
* Command Line Tools alone have no simulator, SDKs or xcodebuild that builds
|
|
68
|
+
* apps. Throws IOS-002 otherwise.
|
|
69
|
+
*/
|
|
70
|
+
function assertFullXcode({
|
|
71
|
+
run = probe
|
|
72
|
+
} = {}) {
|
|
73
|
+
const {
|
|
74
|
+
status,
|
|
75
|
+
output
|
|
76
|
+
} = run("xcode-select", ["-p"]);
|
|
77
|
+
const dir = status === 0 ? output.split("\n")[0].trim() : "";
|
|
78
|
+
if (!/\.app\/Contents\/Developer\/?$/.test(dir)) {
|
|
79
|
+
throw (0, _utils.nativeError)("IOS-002", dir ? `xcode-select points at ${dir}` : "xcode-select has no developer directory");
|
|
80
|
+
}
|
|
81
|
+
return dir;
|
|
82
|
+
}
|
|
83
|
+
const XCODE_SETUP_STEPS = [{
|
|
84
|
+
label: "the Xcode licence",
|
|
85
|
+
consent: "licence",
|
|
86
|
+
flag: "--accept-licenses",
|
|
87
|
+
question: "Accept the Xcode licence (runs sudo xcodebuild -license accept)",
|
|
88
|
+
check: ["xcodebuild", "-license", "check"],
|
|
89
|
+
fix: ["sudo", "xcodebuild", "-license", "accept"]
|
|
90
|
+
}, {
|
|
91
|
+
label: "Xcode's first-launch components",
|
|
92
|
+
consent: "install",
|
|
93
|
+
flag: "--yes",
|
|
94
|
+
question: "Install Xcode's first-launch components (runs sudo xcodebuild -runFirstLaunch)",
|
|
95
|
+
check: ["xcodebuild", "-checkFirstLaunchStatus"],
|
|
96
|
+
fix: ["sudo", "xcodebuild", "-runFirstLaunch"]
|
|
97
|
+
}];
|
|
98
|
+
|
|
99
|
+
/** The setup steps Xcode still needs, each with the sudo command that does it. */
|
|
100
|
+
function pendingXcodeSetup({
|
|
101
|
+
run = probe
|
|
102
|
+
} = {}) {
|
|
103
|
+
return XCODE_SETUP_STEPS.filter(step => run(step.check[0], step.check.slice(1)).status !== 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Full Xcode, licence accepted and first launch done.
|
|
108
|
+
*
|
|
109
|
+
* Each pending step is printed with its sudo command, then asked for: the
|
|
110
|
+
* licence with askConsent("licence") (only --accept-licenses answers it
|
|
111
|
+
* without a terminal), the first-launch install with askConsent("install")
|
|
112
|
+
* (--yes). In a terminal sudo asks for the password there. Without one the
|
|
113
|
+
* command runs as `sudo -n`, which works only where sudo needs no password
|
|
114
|
+
* (hosted CI runners); otherwise the error names the command to run.
|
|
115
|
+
*
|
|
116
|
+
* @param {Record<string, any>} [options] - `flags` ({ yes, acceptLicenses } from parseNativeArgs) and `log`; the rest are seams for tests
|
|
117
|
+
*/
|
|
118
|
+
async function ensureXcodeReady({
|
|
119
|
+
flags = {},
|
|
120
|
+
run = probe,
|
|
121
|
+
isTTY = (0, _utils.isInteractive)(),
|
|
122
|
+
ask = _utils.askConsent,
|
|
123
|
+
runInherited = _utils.runInheritedCommand,
|
|
124
|
+
log = () => {}
|
|
125
|
+
} = {}) {
|
|
126
|
+
const developerDir = assertFullXcode({
|
|
127
|
+
run
|
|
128
|
+
});
|
|
129
|
+
const pending = pendingXcodeSetup({
|
|
130
|
+
run
|
|
131
|
+
});
|
|
132
|
+
if (!pending.length) return developerDir;
|
|
133
|
+
for (const step of pending) {
|
|
134
|
+
const command = step.fix.join(" ");
|
|
135
|
+
log(`Xcode needs ${step.label}. To do it yourself, run: ${command}`, "note");
|
|
136
|
+
const answered = step.consent === "licence" ? flags.acceptLicenses : flags.yes;
|
|
137
|
+
if (!isTTY && !answered) {
|
|
138
|
+
throw (0, _utils.needsAnswerError)(step.question, {
|
|
139
|
+
flag: step.flag,
|
|
140
|
+
hint: `Run ${command} yourself, or pass ${step.flag} where sudo needs no password (CI), or run the build in a terminal.`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (!(await ask(step.consent, step.question, {
|
|
144
|
+
flags,
|
|
145
|
+
isTTY
|
|
146
|
+
}))) {
|
|
147
|
+
throw (0, _utils.nativeError)("IOS-003", command);
|
|
148
|
+
}
|
|
149
|
+
const argv = isTTY ? step.fix : ["sudo", "-n", ...step.fix.slice(1)];
|
|
150
|
+
try {
|
|
151
|
+
await runInherited(argv[0], argv.slice(1));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw (0, _utils.nativeError)("IOS-003", `${command} did not finish (${error.message})`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const still = pendingXcodeSetup({
|
|
157
|
+
run
|
|
158
|
+
});
|
|
159
|
+
if (still.length) throw (0, _utils.nativeError)("IOS-003", still.map(step => step.fix.join(" ")).join(" && "));
|
|
160
|
+
return developerDir;
|
|
161
|
+
}
|
|
162
|
+
function compareVersions(a, b) {
|
|
163
|
+
const pa = String(a).split(".").map(Number);
|
|
164
|
+
const pb = String(b).split(".").map(Number);
|
|
165
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
166
|
+
const diff = (pa[i] || 0) - (pb[i] || 0);
|
|
167
|
+
if (diff) return diff;
|
|
168
|
+
}
|
|
169
|
+
return 0;
|
|
170
|
+
}
|
|
171
|
+
const isAvailable = item => item.isAvailable === true || item.availability === "(available)" || item.availability === "available";
|
|
172
|
+
|
|
173
|
+
/** Available iOS simulator runtimes, newest first. */
|
|
174
|
+
function availableIosRuntimes(runtimesJson) {
|
|
175
|
+
return (runtimesJson?.runtimes || []).filter(runtime => (runtime.platform === "iOS" || /SimRuntime\.iOS-/.test(runtime.identifier)) && isAvailable(runtime)).sort((a, b) => compareVersions(b.version, a.version));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The newest available iOS runtime. With none, `xcodebuild -downloadPlatform
|
|
180
|
+
* iOS` (several GB) runs after askConsent("install") (a terminal yes or
|
|
181
|
+
* --yes). Without a terminal and without --yes it fails with NATIVE-005; a
|
|
182
|
+
* no fails with IOS-004.
|
|
183
|
+
*/
|
|
184
|
+
async function ensureIosRuntime({
|
|
185
|
+
flags = {},
|
|
186
|
+
isTTY = (0, _utils.isInteractive)(),
|
|
187
|
+
ask = _utils.askConsent,
|
|
188
|
+
runInherited = _utils.runInheritedCommand,
|
|
189
|
+
listRuntimes = () => simctlJson(["list", "runtimes"]),
|
|
190
|
+
log = () => {}
|
|
191
|
+
} = {}) {
|
|
192
|
+
let [runtime] = availableIosRuntimes(listRuntimes());
|
|
193
|
+
if (runtime) return runtime;
|
|
194
|
+
const consent = await ask("install", "No iOS simulator runtime is installed. Download the latest (several GB)", {
|
|
195
|
+
flags,
|
|
196
|
+
isTTY
|
|
197
|
+
});
|
|
198
|
+
if (!consent) throw (0, _utils.nativeError)("IOS-004", "the download was declined");
|
|
199
|
+
log("Downloading the iOS simulator runtime", "note");
|
|
200
|
+
await runInherited("xcodebuild", ["-downloadPlatform", "iOS"]);
|
|
201
|
+
[runtime] = availableIosRuntimes(listRuntimes());
|
|
202
|
+
if (!runtime) throw (0, _utils.nativeError)("IOS-004", "xcodebuild -downloadPlatform iOS finished without an available runtime");
|
|
203
|
+
return runtime;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The newest iPhone this runtime supports. simctl lists a runtime's supported
|
|
208
|
+
* device types newest first; the global list is the fallback, ordered by the
|
|
209
|
+
* oldest runtime each type needs.
|
|
210
|
+
*/
|
|
211
|
+
function newestIphoneDeviceType(runtime, deviceTypesJson) {
|
|
212
|
+
const isIphone = type => type.productFamily === "iPhone" || /^iPhone/.test(type.name || "");
|
|
213
|
+
const supported = (runtime?.supportedDeviceTypes || []).filter(isIphone);
|
|
214
|
+
if (supported.length) return supported[0];
|
|
215
|
+
const all = (deviceTypesJson?.devicetypes || []).filter(isIphone);
|
|
216
|
+
return all.sort((a, b) => (b.minRuntimeVersion || 0) - (a.minRuntimeVersion || 0))[0] || null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** Available simulators under a runtime. */
|
|
220
|
+
function simulatorsFor(runtime, devicesJson) {
|
|
221
|
+
return (devicesJson?.devices?.[runtime.identifier] || []).filter(isAvailable);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* The available simulator called `name`: under `runtime` first, then under
|
|
226
|
+
* any other runtime. Null when there is none.
|
|
227
|
+
*/
|
|
228
|
+
function findSimulator(name, runtime, devicesJson) {
|
|
229
|
+
const preferred = runtime ? simulatorsFor(runtime, devicesJson).find(sim => sim.name === name) : null;
|
|
230
|
+
if (preferred) return preferred;
|
|
231
|
+
for (const devices of Object.values(devicesJson?.devices || {})) {
|
|
232
|
+
const match = (devices || []).find(sim => sim.name === name && isAvailable(sim));
|
|
233
|
+
if (match) return match;
|
|
234
|
+
}
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** An iPhone simulator, by device type, else by name (test fixtures carry no type). */
|
|
239
|
+
const isIphoneSimulator = sim => /\.iPhone-/.test(sim?.deviceTypeIdentifier || "") || /^iPhone\b/.test(sim?.name || "");
|
|
240
|
+
|
|
241
|
+
/** "26.0" from "com.apple.CoreSimulator.SimRuntime.iOS-26-0"; null for other platforms. */
|
|
242
|
+
const iosRuntimeVersion = identifier => {
|
|
243
|
+
const match = /SimRuntime\.iOS-([\d-]+)$/.exec(identifier);
|
|
244
|
+
return match ? match[1].replace(/-/g, ".") : null;
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Available iPhone simulators, grouped by iOS runtime: `runtime` first, then
|
|
249
|
+
* the other runtimes newest first.
|
|
250
|
+
*
|
|
251
|
+
* @returns {{ runtimeIdentifier: string, simulators: Record<string, any>[] }[]}
|
|
252
|
+
*/
|
|
253
|
+
function iphoneSimulatorsByRuntime(runtime, devicesJson) {
|
|
254
|
+
return Object.entries(devicesJson?.devices || {}).filter(([identifier]) => identifier === runtime?.identifier || iosRuntimeVersion(identifier)).sort(([a], [b]) => {
|
|
255
|
+
if (a === runtime?.identifier) return -1;
|
|
256
|
+
if (b === runtime?.identifier) return 1;
|
|
257
|
+
return compareVersions(iosRuntimeVersion(b), iosRuntimeVersion(a));
|
|
258
|
+
}).map(([runtimeIdentifier, sims]) => ({
|
|
259
|
+
runtimeIdentifier,
|
|
260
|
+
simulators: (sims || []).filter(sim => isAvailable(sim) && isIphoneSimulator(sim))
|
|
261
|
+
})).filter(group => group.simulators.length);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* The newest of these iPhone simulators, preferring a Pro that is not a Max.
|
|
266
|
+
* Newest is the runtime's supportedDeviceTypes order (simctl lists it newest
|
|
267
|
+
* first), then the model number in the name; name and udid break ties so the
|
|
268
|
+
* pick is the same on every run.
|
|
269
|
+
*/
|
|
270
|
+
function newestIphoneSimulator(simulators, runtime) {
|
|
271
|
+
const order = (runtime?.supportedDeviceTypes || []).map(type => type.identifier);
|
|
272
|
+
const rank = sim => {
|
|
273
|
+
const index = order.indexOf(sim.deviceTypeIdentifier);
|
|
274
|
+
return index === -1 ? order.length : index;
|
|
275
|
+
};
|
|
276
|
+
const model = sim => Number((/^iPhone (\d+)/.exec(sim.name || "") || [])[1] || 0);
|
|
277
|
+
const sorted = [...simulators].sort((a, b) => rank(a) - rank(b) || model(b) - model(a) || String(a.name).localeCompare(String(b.name)) || String(a.udid).localeCompare(String(b.udid)));
|
|
278
|
+
const pro = sorted.find(sim => /\bPro\b/.test(sim.name || "") && !/\bMax\b/.test(sim.name || ""));
|
|
279
|
+
return pro || sorted[0] || null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* The simulator to build for. Install only what is missing, never ask for
|
|
284
|
+
* what can be derived:
|
|
285
|
+
*
|
|
286
|
+
* 1. `name` (ios.simulatorName) when it exists on an available runtime;
|
|
287
|
+
* 2. the remembered pick (`last`, getLastTarget("ios")) while its udid is
|
|
288
|
+
* still available;
|
|
289
|
+
* 3. an iPhone simulator that is already booted;
|
|
290
|
+
* 4. the newest iPhone simulator on `runtime` (a Pro that is not a Max
|
|
291
|
+
* first), else on the newest other runtime that has one;
|
|
292
|
+
* 5. only when there is no iPhone simulator at all, "Catalyst iPhone" is
|
|
293
|
+
* created for the newest iPhone the runtime supports, after
|
|
294
|
+
* askConsent("install").
|
|
295
|
+
*
|
|
296
|
+
* @param {Record<string, any>} [options] - `runtime` (from ensureIosRuntime), `name` (ios.simulatorName), `last`, `flags`, `log`; the rest are seams for tests
|
|
297
|
+
* @returns {Promise<{ name: string, udid: string, created: boolean }>}
|
|
298
|
+
*/
|
|
299
|
+
async function ensureSimulator({
|
|
300
|
+
runtime,
|
|
301
|
+
name,
|
|
302
|
+
last = null,
|
|
303
|
+
flags = {},
|
|
304
|
+
isTTY = (0, _utils.isInteractive)(),
|
|
305
|
+
ask = _utils.askConsent,
|
|
306
|
+
devicesJson = simctlJson(["list", "devices"]),
|
|
307
|
+
deviceTypesJson,
|
|
308
|
+
create = (simName, deviceType, rt) =>
|
|
309
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - simctl with an argv array; the name is passed as one argument.
|
|
310
|
+
(0, _child_process.execFileSync)("xcrun", ["simctl", "create", simName, deviceType.identifier, rt.identifier], {
|
|
311
|
+
encoding: "utf8"
|
|
312
|
+
}),
|
|
313
|
+
log = () => {}
|
|
314
|
+
} = {}) {
|
|
315
|
+
const use = sim => ({
|
|
316
|
+
name: sim.name,
|
|
317
|
+
udid: sim.udid,
|
|
318
|
+
created: false
|
|
319
|
+
});
|
|
320
|
+
if (name) {
|
|
321
|
+
const configured = findSimulator(name, runtime, devicesJson);
|
|
322
|
+
if (configured) return use(configured);
|
|
323
|
+
log(`ios.simulatorName "${name}" is not an available simulator; picking one`, "warning");
|
|
324
|
+
}
|
|
325
|
+
const allAvailable = Object.values(devicesJson?.devices || {}).flat().filter(sim => sim && isAvailable(sim));
|
|
326
|
+
if (last?.target === "simulator" && last.udid) {
|
|
327
|
+
const remembered = allAvailable.find(sim => sim.udid === last.udid);
|
|
328
|
+
if (remembered) return use(remembered);
|
|
329
|
+
}
|
|
330
|
+
const groups = iphoneSimulatorsByRuntime(runtime, devicesJson);
|
|
331
|
+
const booted = groups.flatMap(group => group.simulators).find(sim => sim.state === "Booted");
|
|
332
|
+
if (booted) return use(booted);
|
|
333
|
+
if (groups.length) {
|
|
334
|
+
const [group] = groups;
|
|
335
|
+
const groupRuntime = group.runtimeIdentifier === runtime?.identifier ? runtime : null;
|
|
336
|
+
return use(newestIphoneSimulator(group.simulators, groupRuntime));
|
|
337
|
+
}
|
|
338
|
+
const deviceType = newestIphoneDeviceType(runtime, deviceTypesJson || simctlJson(["list", "devicetypes"]));
|
|
339
|
+
if (!deviceType) throw new Error(`No iPhone device type is available for ${runtime.name}`);
|
|
340
|
+
const consent = await ask("install", `No iPhone simulator is available. Create the "${DEFAULT_SIMULATOR_NAME}" simulator (${deviceType.name || "iPhone"}, ${runtime.name})`, {
|
|
341
|
+
flags,
|
|
342
|
+
isTTY
|
|
343
|
+
});
|
|
344
|
+
if (!consent) {
|
|
345
|
+
throw new Error("No iPhone simulator is available; create one in Xcode (Window > Devices and Simulators)");
|
|
346
|
+
}
|
|
347
|
+
const udid = String(create(DEFAULT_SIMULATOR_NAME, deviceType, runtime) || "").trim();
|
|
348
|
+
log(`Created simulator "${DEFAULT_SIMULATOR_NAME}"`, "success");
|
|
349
|
+
return {
|
|
350
|
+
name: DEFAULT_SIMULATOR_NAME,
|
|
351
|
+
udid,
|
|
352
|
+
created: true
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Apple's identifiers are hex digits and hyphens (a UUID, or the 40-char
|
|
357
|
+
// legacy UDID). The udid ends up in xcodebuild and devicectl arguments, so
|
|
358
|
+
// anything else from devicectl's output is dropped rather than passed on.
|
|
359
|
+
const VALID_UDID_RE = /^[A-Fa-f0-9-]+$/;
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* The iPhones devicectl can reach now, from `devicectl list devices
|
|
363
|
+
* --json-output` JSON. devicectl lists every device it ever paired with and
|
|
364
|
+
* simulators too: keep physical ones whose tunnel is not "unavailable".
|
|
365
|
+
*
|
|
366
|
+
* @returns {{ name: string, udid: string, version: string }[]}
|
|
367
|
+
*/
|
|
368
|
+
function parseDevicectlDevices(json) {
|
|
369
|
+
return (json?.result?.devices || []).filter(d => d.hardwareProperties?.reality === "physical").filter(d => d.connectionProperties?.tunnelState !== "unavailable").filter(d => !d.hardwareProperties?.platform || d.hardwareProperties.platform === "iOS").map(d => ({
|
|
370
|
+
name: d.deviceProperties?.name ?? "iPhone",
|
|
371
|
+
udid: d.hardwareProperties?.udid ?? d.identifier,
|
|
372
|
+
version: d.deviceProperties?.osVersionNumber ?? ""
|
|
373
|
+
})).filter(d => typeof d.udid === "string" && VALID_UDID_RE.test(d.udid));
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* `xcrun devicectl list devices` into a temp file (Apple's scripting
|
|
378
|
+
* guidance), parsed. The file goes in a fresh owner-only folder from
|
|
379
|
+
* mkdtemp, so no other user can predict or pre-create its path.
|
|
380
|
+
*/
|
|
381
|
+
function runDevicectlList({
|
|
382
|
+
exec = _child_process.execFileSync
|
|
383
|
+
} = {}) {
|
|
384
|
+
const dir = _fs.default.mkdtempSync(_path.default.join(_os.default.tmpdir(), "catalyst-devicectl-"));
|
|
385
|
+
const file = _path.default.join(dir, "devices.json");
|
|
386
|
+
try {
|
|
387
|
+
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - fixed argv.
|
|
388
|
+
exec("xcrun", ["devicectl", "list", "devices", "--json-output", file], {
|
|
389
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
390
|
+
timeout: 30000
|
|
391
|
+
});
|
|
392
|
+
return JSON.parse(_fs.default.readFileSync(file, "utf8"));
|
|
393
|
+
} finally {
|
|
394
|
+
_fs.default.rmSync(dir, {
|
|
395
|
+
recursive: true,
|
|
396
|
+
force: true
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/** Connected iPhones; [] when devicectl fails (Xcode older than 15, nothing paired). */
|
|
402
|
+
function listConnectedIphones({
|
|
403
|
+
list = runDevicectlList,
|
|
404
|
+
log = () => {}
|
|
405
|
+
} = {}) {
|
|
406
|
+
try {
|
|
407
|
+
return parseDevicectlDevices(list());
|
|
408
|
+
} catch (error) {
|
|
409
|
+
log(`devicectl could not list devices (${error.message})`, "info");
|
|
410
|
+
return [];
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
const deviceLabel = device => `${device.name}${device.version ? ` (iOS ${device.version})` : ""}`;
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Simulator or connected iPhone. The simulator is the default; `--device`
|
|
417
|
+
* picks the iPhone (one: that one; several: ios.deviceUDID, else a choice in
|
|
418
|
+
* a terminal, else NATIVE-005). Without the flag, a terminal with an iPhone
|
|
419
|
+
* connected is asked, defaulting to the last pick. A connected iPhone never
|
|
420
|
+
* wins on its own.
|
|
421
|
+
*
|
|
422
|
+
* @param {object} options
|
|
423
|
+
* @param {boolean} [options.device] - `--device`
|
|
424
|
+
* @param {{ name: string, udid: string, version?: string }[]} options.devices - connected iPhones
|
|
425
|
+
* @param {string} [options.simulatorName] - for the choice label, when known
|
|
426
|
+
* @param {{ target: string, udid?: string } | null} [options.last] - getLastTarget("ios")
|
|
427
|
+
* @param {string} [options.configuredUdid] - WEBVIEW_CONFIG.ios.deviceUDID
|
|
428
|
+
* @param {boolean} [options.isTTY]
|
|
429
|
+
* @param {typeof selectChoice} [options.select]
|
|
430
|
+
* @returns {Promise<{ target: "simulator" } | { target: "device", udid: string, name: string }>}
|
|
431
|
+
*/
|
|
432
|
+
async function chooseIosTarget({
|
|
433
|
+
device: deviceFlag = false,
|
|
434
|
+
devices = [],
|
|
435
|
+
simulatorName,
|
|
436
|
+
last = null,
|
|
437
|
+
configuredUdid,
|
|
438
|
+
isTTY = (0, _utils.isInteractive)(),
|
|
439
|
+
select = _utils.select
|
|
440
|
+
} = {}) {
|
|
441
|
+
const asTarget = d => ({
|
|
442
|
+
target: "device",
|
|
443
|
+
udid: d.udid,
|
|
444
|
+
name: d.name
|
|
445
|
+
});
|
|
446
|
+
const lastDevice = last?.target === "device" ? devices.find(d => d.udid === last.udid) : null;
|
|
447
|
+
if (deviceFlag) {
|
|
448
|
+
if (!devices.length) {
|
|
449
|
+
throw (0, _utils.nativeError)("IOS-014", "--device was passed but devicectl lists no connected iPhone");
|
|
450
|
+
}
|
|
451
|
+
if (devices.length === 1) return asTarget(devices[0]);
|
|
452
|
+
const configured = configuredUdid ? devices.find(d => d.udid === configuredUdid) : null;
|
|
453
|
+
if (configured) return asTarget(configured);
|
|
454
|
+
if (!isTTY) {
|
|
455
|
+
throw (0, _utils.needsAnswerError)(`${devices.length} iPhones are connected. Which one should the app run on`, {
|
|
456
|
+
hint: "Connect only one iPhone, or set WEBVIEW_CONFIG.ios.deviceUDID to the one to use, or run the build in a terminal to pick."
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const udid = await select("Which iPhone", devices.map(d => ({
|
|
460
|
+
label: deviceLabel(d),
|
|
461
|
+
value: d.udid,
|
|
462
|
+
hint: d.udid
|
|
463
|
+
})), {
|
|
464
|
+
defaultValue: lastDevice?.udid,
|
|
465
|
+
isTTY
|
|
466
|
+
});
|
|
467
|
+
return asTarget(devices.find(d => d.udid === udid));
|
|
468
|
+
}
|
|
469
|
+
if (!isTTY || !devices.length) return {
|
|
470
|
+
target: "simulator"
|
|
471
|
+
};
|
|
472
|
+
const choice = await select("Run the app on", [{
|
|
473
|
+
label: simulatorName ? `${simulatorName} simulator` : "Simulator",
|
|
474
|
+
value: "simulator"
|
|
475
|
+
}, ...devices.map(d => ({
|
|
476
|
+
label: deviceLabel(d),
|
|
477
|
+
value: d.udid,
|
|
478
|
+
hint: "connected"
|
|
479
|
+
}))], {
|
|
480
|
+
defaultValue: lastDevice ? lastDevice.udid : "simulator",
|
|
481
|
+
isTTY
|
|
482
|
+
});
|
|
483
|
+
if (choice === "simulator") return {
|
|
484
|
+
target: "simulator"
|
|
485
|
+
};
|
|
486
|
+
return asTarget(devices.find(d => d.udid === choice));
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* The host a debug build reaches the dev server on. The simulator shares the
|
|
491
|
+
* Mac's network, so localhost. An iPhone uses the Mac's Bonjour name
|
|
492
|
+
* (`<LocalHostName>.local`), else its LAN IP; IOS-015 when there is neither.
|
|
493
|
+
* It goes into the build only, never into config.json.
|
|
494
|
+
*
|
|
495
|
+
* @param {{ target: string, name?: string } | null} target
|
|
496
|
+
* @param {object} [options]
|
|
497
|
+
* @param {() => ({ host: string, source: string } | null)} [options.reachableHost]
|
|
498
|
+
* @param {(message: string, type?: string) => void} [options.log]
|
|
499
|
+
*/
|
|
500
|
+
function devHostFor(target, {
|
|
501
|
+
reachableHost = _setupServer.getDeviceReachableHost,
|
|
502
|
+
log = () => {}
|
|
503
|
+
} = {}) {
|
|
504
|
+
if (target?.target !== "device") return "localhost";
|
|
505
|
+
const found = reachableHost();
|
|
506
|
+
if (!found) {
|
|
507
|
+
throw (0, _utils.nativeError)("IOS-015", `${target.name || "the iPhone"} cannot use localhost, and this Mac has no .local name or LAN address`);
|
|
508
|
+
}
|
|
509
|
+
if (found.source === "lan") {
|
|
510
|
+
log(`No Local Hostname on this Mac; ${target.name || "the iPhone"} loads the dev server from ${found.host}`, "info");
|
|
511
|
+
}
|
|
512
|
+
return found.host;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* A debug build's target and dev server host: list the connected iPhones,
|
|
517
|
+
* choose (chooseIosTarget), find the simulator (ensureSimulator: configured,
|
|
518
|
+
* remembered, booted or newest, creating one only when there is no iPhone
|
|
519
|
+
* simulator), and remember the pick when a person or --device made it.
|
|
520
|
+
*
|
|
521
|
+
* @param {Record<string, any>} [options] - `flags` (parseNativeArgs), `iosConfig` (WEBVIEW_CONFIG.ios: simulatorName, deviceUDID), `cwd` (the app root, for .catalyst/local.json), `log`; the rest are seams for tests
|
|
522
|
+
* @returns {Promise<{ target: { target: "simulator" | "device", name: string, udid: string }, devHost: string }>}
|
|
523
|
+
*/
|
|
524
|
+
async function prepareIosDebugTarget({
|
|
525
|
+
flags = {},
|
|
526
|
+
iosConfig = {},
|
|
527
|
+
cwd = process.cwd(),
|
|
528
|
+
isTTY = (0, _utils.isInteractive)(),
|
|
529
|
+
listDevices = listConnectedIphones,
|
|
530
|
+
last = (0, _localState.getLastTarget)("ios", {
|
|
531
|
+
cwd
|
|
532
|
+
}),
|
|
533
|
+
save = _localState.saveLastTarget,
|
|
534
|
+
select = _utils.select,
|
|
535
|
+
ensureRuntime = ensureIosRuntime,
|
|
536
|
+
ensureSim = ensureSimulator,
|
|
537
|
+
reachableHost = _setupServer.getDeviceReachableHost,
|
|
538
|
+
log = () => {}
|
|
539
|
+
} = {}) {
|
|
540
|
+
const devices = listDevices({
|
|
541
|
+
log
|
|
542
|
+
});
|
|
543
|
+
let target = await chooseIosTarget({
|
|
544
|
+
device: flags.device,
|
|
545
|
+
devices,
|
|
546
|
+
simulatorName: iosConfig.simulatorName || (last?.target === "simulator" ? last.name : undefined),
|
|
547
|
+
last,
|
|
548
|
+
configuredUdid: iosConfig.deviceUDID,
|
|
549
|
+
isTTY,
|
|
550
|
+
select
|
|
551
|
+
});
|
|
552
|
+
if (target.target === "simulator") {
|
|
553
|
+
const runtime = await ensureRuntime({
|
|
554
|
+
flags,
|
|
555
|
+
isTTY,
|
|
556
|
+
log
|
|
557
|
+
});
|
|
558
|
+
const simulator = await ensureSim({
|
|
559
|
+
runtime,
|
|
560
|
+
name: iosConfig.simulatorName,
|
|
561
|
+
last,
|
|
562
|
+
flags,
|
|
563
|
+
isTTY,
|
|
564
|
+
log
|
|
565
|
+
});
|
|
566
|
+
target = {
|
|
567
|
+
target: "simulator",
|
|
568
|
+
name: simulator.name,
|
|
569
|
+
udid: simulator.udid
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
const devHost = devHostFor(target, {
|
|
573
|
+
reachableHost,
|
|
574
|
+
log
|
|
575
|
+
});
|
|
576
|
+
if (flags.device || isTTY) save("ios", target, {
|
|
577
|
+
cwd
|
|
578
|
+
});
|
|
579
|
+
return {
|
|
580
|
+
target,
|
|
581
|
+
devHost
|
|
582
|
+
};
|
|
583
|
+
}
|
|
@@ -29,9 +29,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon
|
|
|
29
29
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor
|
|
30
30
|
|
|
31
31
|
// Code signing
|
|
32
|
-
|
|
33
|
-
CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer
|
|
34
|
-
CODE_SIGN_STYLE = Manual
|
|
32
|
+
CODE_SIGN_STYLE = Automatic
|
|
35
33
|
|
|
36
34
|
// Module settings
|
|
37
35
|
CLANG_ENABLE_MODULES = NO
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
|
+
<key>ITSAppUsesNonExemptEncryption</key>
|
|
6
|
+
<false/>
|
|
5
7
|
<key>NSAppTransportSecurity</key>
|
|
6
8
|
<dict>
|
|
7
9
|
<key>NSAllowsArbitraryLoads</key>
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
|
+
<plist version="1.0">
|
|
4
|
+
<dict>
|
|
5
|
+
<key>NSPrivacyTracking</key>
|
|
6
|
+
<false/>
|
|
7
|
+
<key>NSPrivacyTrackingDomains</key>
|
|
8
|
+
<array/>
|
|
9
|
+
<key>NSPrivacyCollectedDataTypes</key>
|
|
10
|
+
<array/>
|
|
11
|
+
<key>NSPrivacyAccessedAPITypes</key>
|
|
12
|
+
<array>
|
|
13
|
+
<dict>
|
|
14
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
15
|
+
<string>NSPrivacyAccessedAPICategoryUserDefaults</string>
|
|
16
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
17
|
+
<array>
|
|
18
|
+
<string>CA92.1</string>
|
|
19
|
+
</array>
|
|
20
|
+
</dict>
|
|
21
|
+
<dict>
|
|
22
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
23
|
+
<string>NSPrivacyAccessedAPICategorySystemBootTime</string>
|
|
24
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
25
|
+
<array>
|
|
26
|
+
<string>35F9.1</string>
|
|
27
|
+
</array>
|
|
28
|
+
</dict>
|
|
29
|
+
<dict>
|
|
30
|
+
<key>NSPrivacyAccessedAPIType</key>
|
|
31
|
+
<string>NSPrivacyAccessedAPICategoryFileTimestamp</string>
|
|
32
|
+
<key>NSPrivacyAccessedAPITypeReasons</key>
|
|
33
|
+
<array>
|
|
34
|
+
<string>C617.1</string>
|
|
35
|
+
<string>3B52.1</string>
|
|
36
|
+
</array>
|
|
37
|
+
</dict>
|
|
38
|
+
</array>
|
|
39
|
+
</dict>
|
|
40
|
+
</plist>
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3
3
|
<plist version="1.0">
|
|
4
4
|
<dict>
|
|
5
|
-
<key>aps-environment</key>
|
|
6
|
-
<string>development</string>
|
|
7
5
|
<key>keychain-access-groups</key>
|
|
8
6
|
<array>
|
|
9
7
|
<string>$(AppIdentifierPrefix)$(CFBundleIdentifier)</string>
|