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
|
@@ -6,6 +6,7 @@ const {
|
|
|
6
6
|
spawn
|
|
7
7
|
} = require("child_process");
|
|
8
8
|
const fs = require("fs");
|
|
9
|
+
const os = require("os");
|
|
9
10
|
const path = require("path");
|
|
10
11
|
const {
|
|
11
12
|
displayPath
|
|
@@ -14,6 +15,38 @@ const TerminalProgress = require("../terminalProgress.js").default;
|
|
|
14
15
|
const {
|
|
15
16
|
buildFailure
|
|
16
17
|
} = require("../../cli/diagnostic.js");
|
|
18
|
+
const {
|
|
19
|
+
nativeError
|
|
20
|
+
} = require("../utils.js");
|
|
21
|
+
const {
|
|
22
|
+
resolveNativeConfig
|
|
23
|
+
} = require("../nativeConfig.js");
|
|
24
|
+
const {
|
|
25
|
+
resolveCredentials
|
|
26
|
+
} = require("../credentials.js");
|
|
27
|
+
const {
|
|
28
|
+
isReleaseBuildType,
|
|
29
|
+
resolveExportMethod,
|
|
30
|
+
readIosCredentials,
|
|
31
|
+
planIosSigning,
|
|
32
|
+
xcodeAuthArgs,
|
|
33
|
+
setupP12Signing,
|
|
34
|
+
buildExportOptions,
|
|
35
|
+
writeExportOptionsPlist,
|
|
36
|
+
isSigningFailure,
|
|
37
|
+
signingSetupError
|
|
38
|
+
} = require("./signing.js");
|
|
39
|
+
const {
|
|
40
|
+
generateLocalhostP12
|
|
41
|
+
} = require("./localhostCert.js");
|
|
42
|
+
const {
|
|
43
|
+
artifactBaseName,
|
|
44
|
+
releaseOutputDir
|
|
45
|
+
} = require("./output.js");
|
|
46
|
+
const {
|
|
47
|
+
DEFAULT_VERSION_NAME,
|
|
48
|
+
assertReleaseFolderFree
|
|
49
|
+
} = require("../releaseOutput.js");
|
|
17
50
|
const createConfigPhase = require("./config.js");
|
|
18
51
|
const createPluginsPhase = require("./plugins.js");
|
|
19
52
|
const createAssetsPhase = require("./assets.js");
|
|
@@ -26,11 +59,124 @@ const pwd = path.join(catalystCorePath, "dist/native");
|
|
|
26
59
|
const MANAGED_BASELINE_SUFFIX = ".catalyst-base";
|
|
27
60
|
const shellCommand = process.platform === "win32" ? "cmd.exe" : "sh";
|
|
28
61
|
const shellArgs = command => process.platform === "win32" ? ["/d", "/s", "/c", command] : ["-c", command];
|
|
29
|
-
|
|
62
|
+
|
|
63
|
+
// The steps of `catalyst buildApp:ios`. buildAppIos.js creates the progress
|
|
64
|
+
// with these, so the toolchain and target steps show before the build exists.
|
|
65
|
+
const IOS_BUILD_STEPS = {
|
|
66
|
+
toolchain: "Check Xcode",
|
|
67
|
+
target: "Choose simulator or iPhone",
|
|
68
|
+
server: "Start dev server",
|
|
69
|
+
signing: "Resolve signing",
|
|
70
|
+
config: "Generate build configuration",
|
|
71
|
+
launchSimulator: "Launch iOS simulator",
|
|
72
|
+
clean: "Clean build artifacts",
|
|
73
|
+
assets: "Process notification assets",
|
|
74
|
+
build: "Build iOS project",
|
|
75
|
+
findApp: "Locate built application",
|
|
76
|
+
install: "Install application",
|
|
77
|
+
launch: "Launch application",
|
|
78
|
+
archive: "Archive release build",
|
|
79
|
+
export: "Export IPA"
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* @param {object} config - { WEBVIEW_CONFIG, BUILD_OUTPUT_PATH } from config.json
|
|
84
|
+
* @param {object} [options]
|
|
85
|
+
* @param {object} [options.progress] - an existing TerminalProgress to continue (buildAppIos.js passes one made with IOS_BUILD_STEPS)
|
|
86
|
+
* @param {boolean} [options.release] - `--release`: archive and export the store IPA whatever ios.buildType says
|
|
87
|
+
* @param {string} [options.url] - `--url`: the release's production URL, over WEBVIEW_CONFIG.url
|
|
88
|
+
* @param {string} [options.devHost] - debug: the host the app reaches the dev server on (localhost for the simulator, the Mac's `.local` name or LAN IP for an iPhone)
|
|
89
|
+
* @param {{ target: "simulator", name?: string, udid?: string } | { target: "device", name: string, udid: string }} [options.target] - debug: from buildAppIos.js; without it, the configured simulator
|
|
90
|
+
* @param {string} [options.appRoot] - the app root, for credentials/ and build-output/
|
|
91
|
+
* @param {Record<string, string | undefined>} [options.env]
|
|
92
|
+
*/
|
|
93
|
+
function createIosBuild(config, {
|
|
94
|
+
progress: existingProgress,
|
|
95
|
+
release: releaseFlag = false,
|
|
96
|
+
url: urlFlag,
|
|
97
|
+
devHost,
|
|
98
|
+
target,
|
|
99
|
+
appRoot = process.cwd(),
|
|
100
|
+
env = process.env
|
|
101
|
+
} = {}) {
|
|
30
102
|
const {
|
|
31
|
-
WEBVIEW_CONFIG,
|
|
32
103
|
BUILD_OUTPUT_PATH
|
|
33
104
|
} = config;
|
|
105
|
+
const release = Boolean(releaseFlag || isReleaseBuildType(config.WEBVIEW_CONFIG?.ios?.buildType));
|
|
106
|
+
const progress = existingProgress || new TerminalProgress(IOS_BUILD_STEPS, "catalyst buildApp", {
|
|
107
|
+
subject: "ios"
|
|
108
|
+
});
|
|
109
|
+
const log = (message, type) => progress.log(message, type);
|
|
110
|
+
|
|
111
|
+
// ─── Config resolution, before anything reads the config ──────────────────
|
|
112
|
+
// nativeConfig.js turns url/appId/appName into the legacy keys this shell
|
|
113
|
+
// reads, points a debug build at the dev server, and for a release guards
|
|
114
|
+
// the start URL and the app id. All of it fails here, before Xcode runs.
|
|
115
|
+
|
|
116
|
+
const resolved = resolveNativeConfig("ios", {
|
|
117
|
+
release,
|
|
118
|
+
url: urlFlag,
|
|
119
|
+
devHost,
|
|
120
|
+
config,
|
|
121
|
+
cwd: appRoot,
|
|
122
|
+
env,
|
|
123
|
+
log
|
|
124
|
+
});
|
|
125
|
+
// iOS reads the resolved config in memory; Gradle is the one that needs the file.
|
|
126
|
+
resolved.cleanup();
|
|
127
|
+
const WEBVIEW_CONFIG = resolved.WEBVIEW_CONFIG;
|
|
128
|
+
const {
|
|
129
|
+
version,
|
|
130
|
+
usesDevServer
|
|
131
|
+
} = resolved;
|
|
132
|
+
// Before signing files are read or Xcode runs: a second release in the
|
|
133
|
+
// same minute would overwrite the first one's IPA.
|
|
134
|
+
if (release) assertReleaseFolderFree(releaseOutputDir(appRoot, version), [".ipa"]);
|
|
135
|
+
const exportMethod = release ? resolveExportMethod(WEBVIEW_CONFIG.ios || {}, {
|
|
136
|
+
log
|
|
137
|
+
}) : undefined;
|
|
138
|
+
|
|
139
|
+
// Signing files come from credentials/ios/, or in CI from
|
|
140
|
+
// CATALYST_IOS_CREDENTIALS unpacked to a temp folder that cleanup()
|
|
141
|
+
// removes. A release reads them up front; a debug build only when it
|
|
142
|
+
// signs for an iPhone (a simulator build never signs).
|
|
143
|
+
let credentials = null;
|
|
144
|
+
function loadCredentials() {
|
|
145
|
+
if (credentials) return credentials;
|
|
146
|
+
const found = resolveCredentials("ios", {
|
|
147
|
+
cwd: appRoot,
|
|
148
|
+
env,
|
|
149
|
+
log
|
|
150
|
+
});
|
|
151
|
+
try {
|
|
152
|
+
credentials = {
|
|
153
|
+
...readIosCredentials(found.dir, {
|
|
154
|
+
env
|
|
155
|
+
}),
|
|
156
|
+
source: found.source,
|
|
157
|
+
cleanup: found.cleanup
|
|
158
|
+
};
|
|
159
|
+
} catch (error) {
|
|
160
|
+
found.cleanup();
|
|
161
|
+
throw error;
|
|
162
|
+
}
|
|
163
|
+
return credentials;
|
|
164
|
+
}
|
|
165
|
+
if (release) loadCredentials();
|
|
166
|
+
const signingPlan = planIosSigning({
|
|
167
|
+
iosConfig: WEBVIEW_CONFIG.ios || {},
|
|
168
|
+
credentials,
|
|
169
|
+
release,
|
|
170
|
+
exportMethod
|
|
171
|
+
});
|
|
172
|
+
// Automatic signing uses the API key when there is one: cloud signing
|
|
173
|
+
// for a release, development signing for an iPhone.
|
|
174
|
+
const resolveApiKey = () => signingPlan.style === "automatic" ? loadCredentials().apiKey : null;
|
|
175
|
+
const apiKey = release ? resolveApiKey() : null;
|
|
176
|
+
if (release && signingPlan.style === "automatic" && !signingPlan.teamId) {
|
|
177
|
+
credentials?.cleanup();
|
|
178
|
+
throw nativeError("IOS-010", "a signed release with automatic signing needs WEBVIEW_CONFIG.ios.developmentTeam");
|
|
179
|
+
}
|
|
34
180
|
const iosConfig = WEBVIEW_CONFIG.ios;
|
|
35
181
|
const isGoogleSignInEnabled = WEBVIEW_CONFIG.googleSignIn?.enabled ?? false;
|
|
36
182
|
const protocol = WEBVIEW_CONFIG.useHttps ? "https" : "http";
|
|
@@ -41,22 +187,10 @@ function createIosBuild(config) {
|
|
|
41
187
|
const PROJECT_DIR = `${pwd}/iosnativeWebView`;
|
|
42
188
|
const SCHEME_NAME = iosConfig.scheme || "iosnativeWebView";
|
|
43
189
|
const APP_BUNDLE_ID = iosConfig.appBundleId || "com.debug.webview";
|
|
190
|
+
// What Shared.xcconfig sets as PRODUCT_BUNDLE_IDENTIFIER (generateXCConfig).
|
|
191
|
+
const PRODUCT_BUNDLE_ID = iosConfig.bundleIdentifier || iosConfig.appBundleId || "com.catalyst.framework.app";
|
|
44
192
|
const PROJECT_NAME = path.basename(PROJECT_DIR);
|
|
45
|
-
const IPHONE_MODEL = iosConfig.simulatorName;
|
|
46
|
-
const steps = {
|
|
47
|
-
config: "Generate build configuration",
|
|
48
|
-
deviceDetection: "Detect physical device",
|
|
49
|
-
launchSimulator: "Launch iOS simulator",
|
|
50
|
-
clean: "Clean build artifacts",
|
|
51
|
-
assets: "Process notification assets",
|
|
52
|
-
build: "Build iOS project",
|
|
53
|
-
findApp: "Locate built application",
|
|
54
|
-
install: "Install application",
|
|
55
|
-
launch: "Launch application"
|
|
56
|
-
};
|
|
57
|
-
const progress = new TerminalProgress(steps, "catalyst buildApp", {
|
|
58
|
-
subject: "ios"
|
|
59
|
-
});
|
|
193
|
+
const IPHONE_MODEL = target?.target === "simulator" && target.name || iosConfig.simulatorName || "Catalyst iPhone";
|
|
60
194
|
|
|
61
195
|
// ─── Low-level helpers shared across modules ──────────────────────────────
|
|
62
196
|
|
|
@@ -107,12 +241,16 @@ function createIosBuild(config) {
|
|
|
107
241
|
* on the spinner's own row. The full output is still resolved for callers
|
|
108
242
|
* that parse it.
|
|
109
243
|
*/
|
|
110
|
-
function runCommandStreaming(command) {
|
|
244
|
+
function runCommandStreaming(command, args) {
|
|
111
245
|
return new Promise((resolve, reject) => {
|
|
112
246
|
progress.pause();
|
|
113
247
|
|
|
248
|
+
// With `args`, the program runs directly with that argv (no shell
|
|
249
|
+
// quoting to get wrong); without, `command` is a shell string.
|
|
114
250
|
// nosemgrep: javascript.lang.security.detect-child-process.detect-child-process - Internal build command strings, same source as runCommand.
|
|
115
|
-
const child =
|
|
251
|
+
const child = Array.isArray(args) ? spawn(command, args, {
|
|
252
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
253
|
+
}) : spawn(shellCommand, shellArgs(command), {
|
|
116
254
|
stdio: ["ignore", "pipe", "pipe"]
|
|
117
255
|
});
|
|
118
256
|
let output = "";
|
|
@@ -204,7 +342,7 @@ function createIosBuild(config) {
|
|
|
204
342
|
WEBVIEW_CONFIG,
|
|
205
343
|
BUILD_OUTPUT_PATH,
|
|
206
344
|
// Captured before the build chdirs into the Xcode project.
|
|
207
|
-
appRoot
|
|
345
|
+
appRoot,
|
|
208
346
|
iosConfig,
|
|
209
347
|
isGoogleSignInEnabled,
|
|
210
348
|
PROJECT_DIR,
|
|
@@ -215,6 +353,15 @@ function createIosBuild(config) {
|
|
|
215
353
|
url,
|
|
216
354
|
PUBLIC_PATH,
|
|
217
355
|
progress,
|
|
356
|
+
release,
|
|
357
|
+
version,
|
|
358
|
+
target,
|
|
359
|
+
// Debug Info.plist gets NSAllowsLocalNetworking when the app loads the dev server.
|
|
360
|
+
localNetworking: !release && usesDevServer,
|
|
361
|
+
signingPlan,
|
|
362
|
+
apiKey,
|
|
363
|
+
resolveApiKey,
|
|
364
|
+
apsEnvironment: signingPlan.apsEnvironment,
|
|
218
365
|
runCommand,
|
|
219
366
|
runCommandStreaming,
|
|
220
367
|
getXcodeProjectFilePath,
|
|
@@ -248,9 +395,7 @@ function createIosBuild(config) {
|
|
|
248
395
|
buildXcodeProject,
|
|
249
396
|
buildProjectForTesting,
|
|
250
397
|
findAppPath,
|
|
251
|
-
moveAppToBuildOutput,
|
|
252
398
|
installAndLaunchApp,
|
|
253
|
-
detectPhysicalDevices,
|
|
254
399
|
buildProjectForPhysicalDevice,
|
|
255
400
|
findPhysicalDeviceAppPath,
|
|
256
401
|
installAndLaunchOnPhysicalDevice,
|
|
@@ -258,6 +403,19 @@ function createIosBuild(config) {
|
|
|
258
403
|
getBootedSimulatorUUID
|
|
259
404
|
} = createBuildPhase(ctx);
|
|
260
405
|
|
|
406
|
+
/** A fresh self-signed localhost identity for the in-app HTTPS server, every build. */
|
|
407
|
+
function generateLocalhostCertificate() {
|
|
408
|
+
generateLocalhostP12(path.join(PROJECT_DIR, PROJECT_NAME, "localhost.p12"));
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** Remove a leftover file at this path (e.g. dSYMs from a failed export) before writing it. */
|
|
412
|
+
function clearArtifact(file) {
|
|
413
|
+
fs.rmSync(file, {
|
|
414
|
+
recursive: true,
|
|
415
|
+
force: true
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
261
419
|
// ─── Orchestrated build flows ─────────────────────────────────────────────
|
|
262
420
|
|
|
263
421
|
async function buildForIOS(pluginComposition = {}) {
|
|
@@ -272,11 +430,16 @@ function createIosBuild(config) {
|
|
|
272
430
|
await generateXCConfig();
|
|
273
431
|
await copySplashscreenAssets();
|
|
274
432
|
await copyAppIcon();
|
|
433
|
+
generateLocalhostCertificate();
|
|
275
434
|
process.chdir(PROJECT_DIR);
|
|
276
|
-
|
|
435
|
+
// buildAppIos.js chose the target before the config was resolved,
|
|
436
|
+
// so the dev server host fits it. No target means the simulator.
|
|
437
|
+
const physicalDevice = target?.target === "device" ? {
|
|
438
|
+
name: target.name,
|
|
439
|
+
udid: target.udid
|
|
440
|
+
} : null;
|
|
277
441
|
let APP_PATH, targetInfo;
|
|
278
442
|
if (physicalDevice) {
|
|
279
|
-
progress.log("Building for physical device workflow", "success");
|
|
280
443
|
targetInfo = {
|
|
281
444
|
type: "physical",
|
|
282
445
|
name: physicalDevice.name,
|
|
@@ -285,14 +448,17 @@ function createIosBuild(config) {
|
|
|
285
448
|
await cleanBuildArtifacts();
|
|
286
449
|
progress.start("build");
|
|
287
450
|
try {
|
|
451
|
+
if (!ctx.apiKey) ctx.apiKey = ctx.resolveApiKey();
|
|
288
452
|
await buildProjectForPhysicalDevice(SCHEME_NAME, APP_BUNDLE_ID, path.join(process.env.HOME, "Library/Developer/Xcode/DerivedData"), PROJECT_NAME, physicalDevice);
|
|
289
453
|
progress.complete("build");
|
|
290
454
|
} catch (error) {
|
|
291
455
|
// The diagnostic in the catch below shows what xcodebuild
|
|
292
|
-
// said
|
|
293
|
-
//
|
|
294
|
-
|
|
295
|
-
|
|
456
|
+
// said. A signing failure gets IOS-010, which names both
|
|
457
|
+
// ways to set signing up. Not checked up front: an Apple
|
|
458
|
+
// account signed in to Xcode signs fine without either.
|
|
459
|
+
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the device build", error) : error;
|
|
460
|
+
failure.stage = failure.stage || "Building for the device";
|
|
461
|
+
throw failure;
|
|
296
462
|
}
|
|
297
463
|
progress.start("findApp");
|
|
298
464
|
try {
|
|
@@ -315,23 +481,22 @@ function createIosBuild(config) {
|
|
|
315
481
|
APP_PATH = await findAppPath();
|
|
316
482
|
progress.log("Found app at: " + APP_PATH, "success");
|
|
317
483
|
await installAndLaunchApp(APP_PATH);
|
|
318
|
-
const MOVED_APP_PATH = await moveAppToBuildOutput(APP_PATH);
|
|
319
|
-
APP_PATH = MOVED_APP_PATH;
|
|
320
484
|
}
|
|
321
485
|
// Same aligned key/value shape as the Android summary and the
|
|
322
|
-
// Config tree the setup commands print.
|
|
486
|
+
// Config tree the setup commands print. The .app stays in
|
|
487
|
+
// DerivedData: a debug build copies nothing into the app.
|
|
323
488
|
const deviceName = `${targetInfo.name}${targetInfo.type === "physical" ? "" : " simulator"}`;
|
|
324
489
|
progress.printTreeContent("Build", [{
|
|
325
490
|
text: `device ${deviceName}`,
|
|
326
491
|
color: "gray"
|
|
327
492
|
}, {
|
|
328
|
-
text: `
|
|
493
|
+
text: `app ${displayPath(APP_PATH, originalDir)}`,
|
|
329
494
|
color: "gray"
|
|
330
495
|
}, {
|
|
331
496
|
text: `loads ${url}`,
|
|
332
497
|
color: "gray"
|
|
333
498
|
}]);
|
|
334
|
-
progress.summary(`Installed on ${deviceName}`, "
|
|
499
|
+
progress.summary(`Installed on ${deviceName}`, usesDevServer ? "Edit your code; the app reloads from the dev server" : "Open the app");
|
|
335
500
|
return {
|
|
336
501
|
success: true,
|
|
337
502
|
targetInfo,
|
|
@@ -352,6 +517,152 @@ function createIosBuild(config) {
|
|
|
352
517
|
process.chdir(originalDir);
|
|
353
518
|
}
|
|
354
519
|
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* The store build: `xcodebuild archive` in Release for a generic iOS
|
|
523
|
+
* device, then `-exportArchive` to an IPA, with the IPA and zipped dSYMs
|
|
524
|
+
* written to build-output/ios/<version>-<build>/.
|
|
525
|
+
*/
|
|
526
|
+
async function buildReleaseForIOS(pluginComposition = {}) {
|
|
527
|
+
const originalDir = process.cwd();
|
|
528
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - fixed prefix under the OS temp dir.
|
|
529
|
+
const workDir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-ios-release-"));
|
|
530
|
+
let p12Signing = null;
|
|
531
|
+
try {
|
|
532
|
+
await generatePackageSwift(pluginComposition.iosDependencies);
|
|
533
|
+
await updateXcodeProjectPackageDependencies();
|
|
534
|
+
progress.start("assets");
|
|
535
|
+
await processNotificationAssets(WEBVIEW_CONFIG);
|
|
536
|
+
await copyOfflinePage();
|
|
537
|
+
progress.complete("assets");
|
|
538
|
+
progress.start("signing");
|
|
539
|
+
const signing = {
|
|
540
|
+
...signingPlan
|
|
541
|
+
};
|
|
542
|
+
if (signingPlan.p12) {
|
|
543
|
+
p12Signing = setupP12Signing({
|
|
544
|
+
p12: credentials.p12,
|
|
545
|
+
bundleId: PRODUCT_BUNDLE_ID,
|
|
546
|
+
env,
|
|
547
|
+
log
|
|
548
|
+
});
|
|
549
|
+
signing.provisioningProfile = p12Signing.profile.name;
|
|
550
|
+
signing.identity = p12Signing.identity.sha1;
|
|
551
|
+
signing.teamId = p12Signing.profile.teamId || signing.teamId;
|
|
552
|
+
} else if (signing.style === "manual" && exportMethod !== "debugging") {
|
|
553
|
+
signing.identity = "iPhone Distribution";
|
|
554
|
+
}
|
|
555
|
+
const from = credentials?.source === "env" ? "CATALYST_IOS_CREDENTIALS" : "credentials/ios";
|
|
556
|
+
if (signingPlan.p12) {
|
|
557
|
+
progress.log(`Local signing with the .p12 and profile from ${from}`, "info");
|
|
558
|
+
} else if (signing.style === "manual") {
|
|
559
|
+
progress.log(`Manual signing with profile "${signing.provisioningProfile}"`, "info");
|
|
560
|
+
} else {
|
|
561
|
+
progress.log(ctx.apiKey ? `Cloud signing for team ${signing.teamId} with App Store Connect key ${ctx.apiKey.keyId} from ${from}` : `Automatic signing for team ${signing.teamId} with the Apple account signed in to Xcode`, "info");
|
|
562
|
+
}
|
|
563
|
+
progress.complete("signing");
|
|
564
|
+
await generateXCConfig({
|
|
565
|
+
signing,
|
|
566
|
+
version
|
|
567
|
+
});
|
|
568
|
+
await copySplashscreenAssets();
|
|
569
|
+
await copyAppIcon({
|
|
570
|
+
release: true
|
|
571
|
+
});
|
|
572
|
+
generateLocalhostCertificate();
|
|
573
|
+
process.chdir(PROJECT_DIR);
|
|
574
|
+
const outDir = releaseOutputDir(appRoot, version);
|
|
575
|
+
fs.mkdirSync(outDir, {
|
|
576
|
+
recursive: true
|
|
577
|
+
});
|
|
578
|
+
const name = artifactBaseName(iosConfig.appName);
|
|
579
|
+
const archivePath = path.join(workDir, `${name}.xcarchive`);
|
|
580
|
+
const signingArgs = signing.style === "automatic" ? xcodeAuthArgs(ctx.apiKey) : [];
|
|
581
|
+
progress.start("archive");
|
|
582
|
+
try {
|
|
583
|
+
await runCommandStreaming("xcodebuild", ["archive", "-project", `${PROJECT_NAME}.xcodeproj`, "-scheme", SCHEME_NAME, "-configuration", "Release", "-destination", "generic/platform=iOS", "-archivePath", archivePath, ...signingArgs]);
|
|
584
|
+
} catch (error) {
|
|
585
|
+
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the release archive", error) : error;
|
|
586
|
+
failure.stage = "Archiving the release build";
|
|
587
|
+
throw failure;
|
|
588
|
+
}
|
|
589
|
+
progress.complete("archive");
|
|
590
|
+
progress.start("export");
|
|
591
|
+
const exportPath = path.join(workDir, "export");
|
|
592
|
+
const optionsPlist = writeExportOptionsPlist(buildExportOptions({
|
|
593
|
+
method: exportMethod,
|
|
594
|
+
teamId: signing.teamId,
|
|
595
|
+
signingStyle: signing.style,
|
|
596
|
+
bundleId: PRODUCT_BUNDLE_ID,
|
|
597
|
+
profileName: signing.provisioningProfile,
|
|
598
|
+
signingCertificate: signing.p12 ? signing.identity : undefined
|
|
599
|
+
}), path.join(workDir, "ExportOptions.plist"));
|
|
600
|
+
try {
|
|
601
|
+
await runCommandStreaming("xcodebuild", ["-exportArchive", "-archivePath", archivePath, "-exportPath", exportPath, "-exportOptionsPlist", optionsPlist, ...(signing.style === "automatic" ? xcodeAuthArgs(ctx.apiKey) : [])]);
|
|
602
|
+
} catch (error) {
|
|
603
|
+
const failure = isSigningFailure(error.output) ? signingSetupError("Xcode could not sign the IPA for export", error) : error;
|
|
604
|
+
failure.stage = "Exporting the IPA";
|
|
605
|
+
throw failure;
|
|
606
|
+
}
|
|
607
|
+
const exported = fs.readdirSync(exportPath).find(file => file.endsWith(".ipa"));
|
|
608
|
+
if (!exported) throw new Error(`xcodebuild -exportArchive wrote no .ipa to ${exportPath}`);
|
|
609
|
+
const ipaPath = path.join(outDir, `${name}.ipa`);
|
|
610
|
+
clearArtifact(ipaPath);
|
|
611
|
+
fs.copyFileSync(path.join(exportPath, exported), ipaPath);
|
|
612
|
+
const dsymsDir = path.join(archivePath, "dSYMs");
|
|
613
|
+
const dsymsZip = path.join(outDir, `${name}-dSYMs.zip`);
|
|
614
|
+
clearArtifact(dsymsZip);
|
|
615
|
+
if (fs.existsSync(dsymsDir) && fs.readdirSync(dsymsDir).length > 0) {
|
|
616
|
+
execFileSync("ditto", ["-c", "-k", "--keepParent", dsymsDir, dsymsZip]);
|
|
617
|
+
}
|
|
618
|
+
progress.complete("export");
|
|
619
|
+
const rows = [{
|
|
620
|
+
text: `artifact ${displayPath(ipaPath, originalDir)}`,
|
|
621
|
+
color: "gray"
|
|
622
|
+
}];
|
|
623
|
+
if (fs.existsSync(dsymsZip)) {
|
|
624
|
+
rows.push({
|
|
625
|
+
text: `dSYMs ${displayPath(dsymsZip, originalDir)}`,
|
|
626
|
+
color: "gray"
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
rows.push({
|
|
630
|
+
text: `version ${version.versionName || DEFAULT_VERSION_NAME} (${version.buildNumber})`,
|
|
631
|
+
color: "gray"
|
|
632
|
+
});
|
|
633
|
+
rows.push({
|
|
634
|
+
text: `signing ${signing.style}, ${exportMethod}`,
|
|
635
|
+
color: "gray"
|
|
636
|
+
});
|
|
637
|
+
rows.push({
|
|
638
|
+
text: `loads ${url}`,
|
|
639
|
+
color: "gray"
|
|
640
|
+
});
|
|
641
|
+
progress.printTreeContent("Build", rows);
|
|
642
|
+
progress.summary("Built the signed IPA", "Upload it to App Store Connect with Transporter");
|
|
643
|
+
return {
|
|
644
|
+
success: true,
|
|
645
|
+
ipaPath,
|
|
646
|
+
dsymsZip: fs.existsSync(dsymsZip) ? dsymsZip : null
|
|
647
|
+
};
|
|
648
|
+
} catch (error) {
|
|
649
|
+
if (progress.currentStep) progress.fail(progress.currentStep.id);
|
|
650
|
+
process.stderr.write(buildFailure({
|
|
651
|
+
error,
|
|
652
|
+
stage: error.stage || "iOS release build",
|
|
653
|
+
code: "IOS-000",
|
|
654
|
+
cwd: originalDir
|
|
655
|
+
}));
|
|
656
|
+
throw error;
|
|
657
|
+
} finally {
|
|
658
|
+
p12Signing?.cleanup();
|
|
659
|
+
fs.rmSync(workDir, {
|
|
660
|
+
recursive: true,
|
|
661
|
+
force: true
|
|
662
|
+
});
|
|
663
|
+
process.chdir(originalDir);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
355
666
|
async function buildIosForTesting(pluginComposition = {}) {
|
|
356
667
|
const originalDir = process.cwd();
|
|
357
668
|
try {
|
|
@@ -366,6 +677,7 @@ function createIosBuild(config) {
|
|
|
366
677
|
await generateXCConfig();
|
|
367
678
|
await copySplashscreenAssets();
|
|
368
679
|
await copyAppIcon();
|
|
680
|
+
generateLocalhostCertificate();
|
|
369
681
|
progress.log("Changing directory to: " + PROJECT_DIR, "info");
|
|
370
682
|
process.chdir(PROJECT_DIR);
|
|
371
683
|
await cleanBuildArtifacts();
|
|
@@ -409,14 +721,22 @@ function createIosBuild(config) {
|
|
|
409
721
|
copySplashscreenAssets,
|
|
410
722
|
copyAppIcon,
|
|
411
723
|
buildForIOS,
|
|
724
|
+
buildReleaseForIOS,
|
|
412
725
|
buildIosForTesting,
|
|
413
726
|
progress,
|
|
727
|
+
release,
|
|
728
|
+
version,
|
|
414
729
|
WEBVIEW_CONFIG,
|
|
730
|
+
/** Debug: the app loads the dev server, which buildAppIos.js makes sure answers. */
|
|
731
|
+
usesDevServer,
|
|
415
732
|
PROJECT_DIR,
|
|
416
|
-
SCHEME_NAME
|
|
733
|
+
SCHEME_NAME,
|
|
734
|
+
/** Removes the credentials unpacked from CATALYST_IOS_CREDENTIALS. */
|
|
735
|
+
cleanup: () => credentials?.cleanup()
|
|
417
736
|
};
|
|
418
737
|
}
|
|
419
738
|
module.exports = {
|
|
420
739
|
createIosBuild,
|
|
740
|
+
IOS_BUILD_STEPS,
|
|
421
741
|
pwd
|
|
422
742
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// The shell's in-app HTTPS server (FrameworkServerUtils.swift) serves on
|
|
4
|
+
// localhost with the identity in localhost.p12, imported with an empty
|
|
5
|
+
// passphrase. WKWebView accepts it because WebViewNavigationDelegate trusts
|
|
6
|
+
// any server certificate for localhost/127.0.0.1 (no pinning), so a fresh
|
|
7
|
+
// self-signed pair per build works and no private key has to live in git.
|
|
8
|
+
//
|
|
9
|
+
// The PKCS#12 uses SHA1-3DES and a SHA-1 MAC: the format SecPKCS12Import has
|
|
10
|
+
// always read, and one OpenSSL 3 writes without its legacy provider and
|
|
11
|
+
// LibreSSL (/usr/bin/openssl on macOS) writes by default.
|
|
12
|
+
//
|
|
13
|
+
// Also runnable for CI or a manual Xcode build:
|
|
14
|
+
// node src/native/buildIos/localhostCert.js [destination.p12]
|
|
15
|
+
const {
|
|
16
|
+
execFileSync
|
|
17
|
+
} = require("child_process");
|
|
18
|
+
const fs = require("fs");
|
|
19
|
+
const os = require("os");
|
|
20
|
+
const path = require("path");
|
|
21
|
+
const DEFAULT_DESTINATION = path.join(__dirname, "..", "iosnativeWebView", "iosnativeWebView", "localhost.p12");
|
|
22
|
+
const VALIDITY_DAYS = 3650;
|
|
23
|
+
function defaultRun(command, args) {
|
|
24
|
+
return execFileSync(command, args, {
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** openssl from PATH, else the one macOS ships. */
|
|
31
|
+
function opensslCandidates() {
|
|
32
|
+
return ["openssl", "/usr/bin/openssl"];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Write a new self-signed localhost identity to `destination`.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} [destination]
|
|
39
|
+
* @param {object} [options]
|
|
40
|
+
* @param {(command: string, args: string[]) => string} [options.run]
|
|
41
|
+
* @returns {string} the destination
|
|
42
|
+
*/
|
|
43
|
+
function generateLocalhostP12(destination = DEFAULT_DESTINATION, {
|
|
44
|
+
run = defaultRun
|
|
45
|
+
} = {}) {
|
|
46
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "catalyst-localhost-cert-"));
|
|
47
|
+
fs.chmodSync(dir, 0o700);
|
|
48
|
+
const keyFile = path.join(dir, "key.pem");
|
|
49
|
+
const certFile = path.join(dir, "cert.pem");
|
|
50
|
+
const p12File = path.join(dir, "localhost.p12");
|
|
51
|
+
try {
|
|
52
|
+
let lastError = null;
|
|
53
|
+
for (const openssl of opensslCandidates()) {
|
|
54
|
+
try {
|
|
55
|
+
run(openssl, ["req", "-x509", "-newkey", "rsa:2048", "-nodes", "-keyout", keyFile, "-out", certFile, "-days", String(VALIDITY_DAYS), "-subj", "/O=Catalyst/CN=localhost", "-addext", "subjectAltName=DNS:localhost,IP:127.0.0.1", "-addext", "extendedKeyUsage=serverAuth"]);
|
|
56
|
+
run(openssl, ["pkcs12", "-export", "-inkey", keyFile, "-in", certFile, "-name", "localhost", "-out", p12File, "-passout", "pass:", "-certpbe", "PBE-SHA1-3DES", "-keypbe", "PBE-SHA1-3DES", "-macalg", "sha1"]);
|
|
57
|
+
lastError = null;
|
|
58
|
+
break;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
lastError = error;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (lastError) {
|
|
64
|
+
const detail = String(lastError.stderr || lastError.message || "").trim().split("\n").pop();
|
|
65
|
+
throw new Error(`Could not create the localhost certificate with openssl: ${detail}`);
|
|
66
|
+
}
|
|
67
|
+
fs.mkdirSync(path.dirname(destination), {
|
|
68
|
+
recursive: true
|
|
69
|
+
});
|
|
70
|
+
fs.copyFileSync(p12File, destination);
|
|
71
|
+
return destination;
|
|
72
|
+
} finally {
|
|
73
|
+
fs.rmSync(dir, {
|
|
74
|
+
recursive: true,
|
|
75
|
+
force: true
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
module.exports = {
|
|
80
|
+
generateLocalhostP12,
|
|
81
|
+
DEFAULT_DESTINATION
|
|
82
|
+
};
|
|
83
|
+
if (require.main === module) {
|
|
84
|
+
const destination = path.resolve(process.argv[2] || DEFAULT_DESTINATION);
|
|
85
|
+
generateLocalhostP12(destination);
|
|
86
|
+
console.log(`Wrote ${destination}`);
|
|
87
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
Object.defineProperty(exports, "artifactBaseName", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
get: function () {
|
|
9
|
+
return _releaseOutput.artifactBaseName;
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
exports.releaseOutputDir = releaseOutputDir;
|
|
13
|
+
var _path = _interopRequireDefault(require("path"));
|
|
14
|
+
var _releaseOutput = require("../releaseOutput.js");
|
|
15
|
+
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
|
|
16
|
+
/**
|
|
17
|
+
* Where a release writes its IPA and dSYMs: <app>/build-output/ios/<version>-<build>,
|
|
18
|
+
* with the version from package.json and the build number from
|
|
19
|
+
* nativeConfig.js resolveAppVersion (the same pair as CFBundleShortVersionString
|
|
20
|
+
* and CFBundleVersion). Outside build/, which `catalyst build` deletes.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} appRoot
|
|
23
|
+
* @param {{ versionName?: string, buildNumber: number }} version
|
|
24
|
+
*/
|
|
25
|
+
function releaseOutputDir(appRoot, version) {
|
|
26
|
+
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal - appRoot is the local app; the folder name is sanitised.
|
|
27
|
+
return _path.default.resolve(appRoot, "build-output", "ios", (0, _releaseOutput.releaseFolderName)(version));
|
|
28
|
+
}
|