@expo/build-tools 19.1.0 → 20.1.0
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/dist/builders/android.js +6 -0
- package/dist/builders/ios.js +10 -1
- package/dist/datadog.d.ts +9 -1
- package/dist/datadog.js +35 -6
- package/dist/ios/pod.js +4 -0
- package/dist/steps/functions/restoreBuildCache.js +5 -17
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +13 -21
- package/dist/steps/functions/startArgentRemoteSession.js +13 -21
- package/dist/steps/functions/startIosSimulator.js +12 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +2 -2
- package/dist/steps/utils/ios/xcactivitylog.d.ts +6 -4
- package/dist/steps/utils/ios/xcactivitylog.js +32 -4
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +10 -16
- package/dist/steps/utils/remoteDeviceRunSession.js +81 -65
- package/dist/utils/IosSimulatorUtils.d.ts +4 -0
- package/dist/utils/IosSimulatorUtils.js +8 -1
- package/dist/utils/artifacts.js +1 -61
- package/dist/utils/expoUpdatesEmbedded.d.ts +3 -0
- package/dist/utils/expoUpdatesEmbedded.js +109 -0
- package/package.json +9 -8
package/dist/builders/android.js
CHANGED
|
@@ -22,6 +22,7 @@ const saveBuildCache_1 = require("../steps/functions/saveBuildCache");
|
|
|
22
22
|
const gradleConfig_1 = require("../steps/utils/android/gradleConfig");
|
|
23
23
|
const artifacts_1 = require("../utils/artifacts");
|
|
24
24
|
const expoUpdates_1 = require("../utils/expoUpdates");
|
|
25
|
+
const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
|
|
25
26
|
const hooks_1 = require("../utils/hooks");
|
|
26
27
|
const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
|
|
27
28
|
async function androidBuilder(ctx) {
|
|
@@ -177,6 +178,11 @@ async function buildAsync(ctx) {
|
|
|
177
178
|
logger: ctx.logger,
|
|
178
179
|
});
|
|
179
180
|
});
|
|
181
|
+
if (ctx.env.EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE) {
|
|
182
|
+
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_EMBEDDED_BUNDLE, async () => {
|
|
183
|
+
await (0, expoUpdatesEmbedded_1.uploadEmbeddedBundleAsync)(ctx);
|
|
184
|
+
});
|
|
185
|
+
}
|
|
180
186
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.SAVE_CACHE, async () => {
|
|
181
187
|
if (ctx.isLocal) {
|
|
182
188
|
ctx.logger.info('Local builds do not support saving cache.');
|
package/dist/builders/ios.js
CHANGED
|
@@ -26,6 +26,7 @@ const restoreBuildCache_1 = require("../steps/functions/restoreBuildCache");
|
|
|
26
26
|
const saveBuildCache_1 = require("../steps/functions/saveBuildCache");
|
|
27
27
|
const artifacts_1 = require("../utils/artifacts");
|
|
28
28
|
const expoUpdates_1 = require("../utils/expoUpdates");
|
|
29
|
+
const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
|
|
29
30
|
const hooks_1 = require("../utils/hooks");
|
|
30
31
|
const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
|
|
31
32
|
const processes_1 = require("../utils/processes");
|
|
@@ -166,13 +167,16 @@ async function buildAsync(ctx) {
|
|
|
166
167
|
}
|
|
167
168
|
try {
|
|
168
169
|
const { derivedDataPath, workspacePath } = (0, nullthrows_1.default)(fastlaneResult);
|
|
169
|
-
await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
|
|
170
|
+
const { skipped } = await (0, xcactivitylog_1.parseAndReportXcactivitylog)({
|
|
170
171
|
derivedDataPath,
|
|
171
172
|
workspacePath,
|
|
172
173
|
logger: ctx.logger,
|
|
173
174
|
proxyBaseUrl: ctx.env.EAS_BUILD_COCOAPODS_CACHE_URL,
|
|
174
175
|
env: ctx.env,
|
|
175
176
|
});
|
|
177
|
+
if (skipped) {
|
|
178
|
+
ctx.markBuildPhaseSkipped();
|
|
179
|
+
}
|
|
176
180
|
}
|
|
177
181
|
catch (err) {
|
|
178
182
|
sentry_1.Sentry.capture('Failed to parse xcactivitylog', err);
|
|
@@ -189,6 +193,11 @@ async function buildAsync(ctx) {
|
|
|
189
193
|
logger: ctx.logger,
|
|
190
194
|
});
|
|
191
195
|
});
|
|
196
|
+
if (ctx.env.EAS_UPDATE_EXPERIMENTAL_UPLOAD_EMBEDDED_BUNDLE) {
|
|
197
|
+
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_EMBEDDED_BUNDLE, async () => {
|
|
198
|
+
await (0, expoUpdatesEmbedded_1.uploadEmbeddedBundleAsync)(ctx);
|
|
199
|
+
});
|
|
200
|
+
}
|
|
192
201
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.SAVE_CACHE, async () => {
|
|
193
202
|
if (ctx.isLocal) {
|
|
194
203
|
ctx.logger.info('Local builds do not support saving cache.');
|
package/dist/datadog.d.ts
CHANGED
|
@@ -1,11 +1,19 @@
|
|
|
1
|
+
type MetricsTarget = {
|
|
2
|
+
kind: 'build';
|
|
3
|
+
turtleBuildId: string;
|
|
4
|
+
} | {
|
|
5
|
+
kind: 'jobRun';
|
|
6
|
+
turtleJobRunId: string;
|
|
7
|
+
};
|
|
1
8
|
type DatadogSetupOptions = {
|
|
2
9
|
expoApiV2BaseUrl: string;
|
|
3
|
-
turtleBuildId: string;
|
|
4
10
|
robotAccessToken: string;
|
|
11
|
+
target: MetricsTarget;
|
|
5
12
|
};
|
|
6
13
|
export declare const Datadog: {
|
|
7
14
|
setup(opts: DatadogSetupOptions | null): void;
|
|
8
15
|
distribution(name: string, value: number, tags?: Record<string, string>): void;
|
|
16
|
+
log(message: string, tags?: Record<string, string>): void;
|
|
9
17
|
flushAsync(): Promise<void>;
|
|
10
18
|
};
|
|
11
19
|
export {};
|
package/dist/datadog.js
CHANGED
|
@@ -4,7 +4,7 @@ exports.Datadog = void 0;
|
|
|
4
4
|
const sentry_1 = require("./sentry");
|
|
5
5
|
const turtleFetch_1 = require("./utils/turtleFetch");
|
|
6
6
|
let setupOptions = null;
|
|
7
|
-
let
|
|
7
|
+
let pendingUploads = [];
|
|
8
8
|
exports.Datadog = {
|
|
9
9
|
setup(opts) {
|
|
10
10
|
setupOptions = opts;
|
|
@@ -13,7 +13,7 @@ exports.Datadog = {
|
|
|
13
13
|
if (!setupOptions) {
|
|
14
14
|
return;
|
|
15
15
|
}
|
|
16
|
-
const { expoApiV2BaseUrl,
|
|
16
|
+
const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
|
|
17
17
|
const metrics = [
|
|
18
18
|
{
|
|
19
19
|
name,
|
|
@@ -22,20 +22,49 @@ exports.Datadog = {
|
|
|
22
22
|
tags,
|
|
23
23
|
},
|
|
24
24
|
];
|
|
25
|
-
const
|
|
25
|
+
const metricsPath = target.kind === 'build'
|
|
26
|
+
? `turtle-builds/${target.turtleBuildId}/metrics`
|
|
27
|
+
: `turtle-job-runs/${target.turtleJobRunId}/metrics`;
|
|
28
|
+
const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(metricsPath, expoApiV2BaseUrl).toString(), 'POST', {
|
|
26
29
|
json: { metrics },
|
|
27
30
|
headers: {
|
|
28
31
|
Authorization: `Bearer ${robotAccessToken}`,
|
|
29
32
|
},
|
|
30
33
|
retries: 2,
|
|
31
34
|
}).catch(err => {
|
|
32
|
-
sentry_1.Sentry.capture(
|
|
35
|
+
sentry_1.Sentry.capture(`Failed to report turtle ${target.kind} metric`, err, {
|
|
33
36
|
extras: { metrics },
|
|
34
37
|
});
|
|
35
38
|
});
|
|
36
|
-
|
|
39
|
+
pendingUploads.push(uploadPromise);
|
|
40
|
+
},
|
|
41
|
+
log(message, tags = {}) {
|
|
42
|
+
if (!setupOptions) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { expoApiV2BaseUrl, robotAccessToken, target } = setupOptions;
|
|
46
|
+
const log = target.kind === 'build'
|
|
47
|
+
? { buildId: target.turtleBuildId, message, tags }
|
|
48
|
+
: { jobRunId: target.turtleJobRunId, message, tags };
|
|
49
|
+
const logsPath = target.kind === 'build' ? 'turtle-builds/logs' : 'turtle-job-runs/logs';
|
|
50
|
+
const uploadPromise = (0, turtleFetch_1.turtleFetch)(new URL(logsPath, expoApiV2BaseUrl).toString(), 'POST', {
|
|
51
|
+
json: log,
|
|
52
|
+
headers: {
|
|
53
|
+
Authorization: `Bearer ${robotAccessToken}`,
|
|
54
|
+
},
|
|
55
|
+
shouldThrowOnNotOk: false,
|
|
56
|
+
}).catch(err => {
|
|
57
|
+
sentry_1.Sentry.capture(`Failed to report turtle ${target.kind} log`, err, {
|
|
58
|
+
extras: { log },
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
pendingUploads.push(uploadPromise);
|
|
37
62
|
},
|
|
38
63
|
async flushAsync() {
|
|
39
|
-
|
|
64
|
+
// Rotate so each flush only awaits its own batch; uploads enqueued during the
|
|
65
|
+
// await land in the fresh array for a later flush.
|
|
66
|
+
const uploads = pendingUploads;
|
|
67
|
+
pendingUploads = [];
|
|
68
|
+
await Promise.allSettled(uploads);
|
|
40
69
|
},
|
|
41
70
|
};
|
package/dist/ios/pod.js
CHANGED
|
@@ -38,6 +38,10 @@ async function installPods(ctx, { infoCallbackFn }) {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
async function resolvePrecompiledModulesPodInstallEnvAsync(ctx) {
|
|
41
|
+
if (ctx.env.EXPO_USE_PRECOMPILED_MODULES === '0') {
|
|
42
|
+
ctx.logger.info('EXPO_USE_PRECOMPILED_MODULES=0 is set; not enabling precompiled modules use.');
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
41
45
|
if (ctx.job.builderEnvironment?.env?.EAS_USE_PRECOMPILED_MODULES !== '1') {
|
|
42
46
|
return {};
|
|
43
47
|
}
|
|
@@ -18,6 +18,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
18
|
const ccacheStats_1 = require("./ccacheStats");
|
|
19
19
|
const restoreCache_1 = require("./restoreCache");
|
|
20
20
|
const cacheKey_1 = require("../../utils/cacheKey");
|
|
21
|
+
const datadog_1 = require("../../datadog");
|
|
21
22
|
const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
|
|
22
23
|
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
23
24
|
function createRestoreBuildCacheFunction() {
|
|
@@ -216,23 +217,10 @@ async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets,
|
|
|
216
217
|
});
|
|
217
218
|
const hitType = matchedKey === cacheKey ? 'direct_hit' : 'prefix_match';
|
|
218
219
|
logger.info(`Gradle cache restored to ${gradleCachesPath} (${hitType === 'direct_hit' ? 'direct hit' : 'prefix match'})`);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
message: `Gradle cache restored (${hitType})`,
|
|
224
|
-
tags: {
|
|
225
|
-
event: 'gradle_cache_restored',
|
|
226
|
-
cache_hit_type: hitType,
|
|
227
|
-
},
|
|
228
|
-
},
|
|
229
|
-
headers: {
|
|
230
|
-
Authorization: `Bearer ${robotAccessToken}`,
|
|
231
|
-
},
|
|
232
|
-
shouldThrowOnNotOk: false,
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
catch { }
|
|
220
|
+
datadog_1.Datadog.log(`Gradle cache restored (${hitType})`, {
|
|
221
|
+
event: 'gradle_cache_restored',
|
|
222
|
+
cache_hit_type: hitType,
|
|
223
|
+
});
|
|
236
224
|
}
|
|
237
225
|
catch (err) {
|
|
238
226
|
if (err instanceof turtleFetch_1.TurtleFetchError && err.response?.status === 404) {
|
|
@@ -29,10 +29,13 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
29
29
|
}),
|
|
30
30
|
],
|
|
31
31
|
fn: async ({ logger, global }, { inputs, env }) => {
|
|
32
|
-
// Fail fast before any expensive setup if the
|
|
33
|
-
//
|
|
34
|
-
//
|
|
32
|
+
// Fail fast before any expensive setup if the injected env
|
|
33
|
+
// vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
|
|
34
|
+
// back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
|
|
35
|
+
// for our ngrok tunnels), and NGROK_AUTHTOKEN (to authenticate them).
|
|
35
36
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
37
|
+
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
38
|
+
const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
|
|
36
39
|
const packageVersion = inputs.package_version.value;
|
|
37
40
|
const { runtimePlatform } = global;
|
|
38
41
|
logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
|
|
@@ -40,12 +43,6 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
40
43
|
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
41
44
|
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
42
45
|
}
|
|
43
|
-
logger.info('Ensuring cloudflared is installed.');
|
|
44
|
-
const cloudflaredCommand = await (0, remoteDeviceRunSession_1.ensureCloudflaredInstalledAsync)({
|
|
45
|
-
runtimePlatform,
|
|
46
|
-
env,
|
|
47
|
-
logger,
|
|
48
|
-
});
|
|
49
46
|
logger.info(packageVersion
|
|
50
47
|
? `Cloning agent-device @ v${packageVersion} into ${SRC_DIR}.`
|
|
51
48
|
: `Cloning agent-device (latest) into ${SRC_DIR}.`);
|
|
@@ -71,18 +68,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
71
68
|
parse: parseDaemonInfo,
|
|
72
69
|
});
|
|
73
70
|
logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
logger.info('Waiting for a public tunnel URL.');
|
|
81
|
-
const agentDeviceRemoteSessionUrl = await (0, remoteDeviceRunSession_1.waitForMatchInOutputAsync)({
|
|
82
|
-
process: cloudflared,
|
|
83
|
-
pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
|
|
84
|
-
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
85
|
-
description: 'cloudflared tunnel',
|
|
71
|
+
const agentDeviceRemoteSessionUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
|
|
72
|
+
port: daemonPort,
|
|
73
|
+
subdomainPrefix: 'agent-device',
|
|
74
|
+
baseDomain: ngrokTunnelDomain,
|
|
75
|
+
authtoken: ngrokAuthtoken,
|
|
76
|
+
logger,
|
|
86
77
|
});
|
|
87
78
|
logger.info(`Tunnel is ready at ${agentDeviceRemoteSessionUrl}.`);
|
|
88
79
|
// serve-sim is iOS-only — only launch it (and report a webPreviewUrl)
|
|
@@ -90,6 +81,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
90
81
|
let webPreviewUrl;
|
|
91
82
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
92
83
|
const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
|
|
84
|
+
baseDomain: ngrokTunnelDomain,
|
|
93
85
|
env,
|
|
94
86
|
logger,
|
|
95
87
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
@@ -31,10 +31,13 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
31
31
|
}),
|
|
32
32
|
],
|
|
33
33
|
fn: async ({ logger, global }, { inputs, env }) => {
|
|
34
|
-
// Fail fast before any expensive setup if the
|
|
35
|
-
//
|
|
36
|
-
//
|
|
34
|
+
// Fail fast before any expensive setup if the injected env
|
|
35
|
+
// vars are missing: DEVICE_RUN_SESSION_ID (to report the remote config
|
|
36
|
+
// back to the API server), EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN (base domain
|
|
37
|
+
// for our ngrok tunnels), and NGROK_AUTHTOKEN (to authenticate them).
|
|
37
38
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
39
|
+
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
40
|
+
const ngrokAuthtoken = (0, remoteDeviceRunSession_1.getNgrokAuthtokenOrThrow)(env);
|
|
38
41
|
const packageVersion = inputs.package_version.value;
|
|
39
42
|
const versionSpec = packageVersion ?? 'latest';
|
|
40
43
|
const { runtimePlatform } = global;
|
|
@@ -43,12 +46,6 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
43
46
|
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
44
47
|
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
45
48
|
}
|
|
46
|
-
logger.info('Ensuring cloudflared is installed.');
|
|
47
|
-
const cloudflaredCommand = await (0, remoteDeviceRunSession_1.ensureCloudflaredInstalledAsync)({
|
|
48
|
-
runtimePlatform,
|
|
49
|
-
env,
|
|
50
|
-
logger,
|
|
51
|
-
});
|
|
52
49
|
// Stale state from a previous run would mask the new server's port.
|
|
53
50
|
await node_fs_1.default.promises.rm(ARGENT_STATE_FILE, { force: true });
|
|
54
51
|
logger.info(`Launching ${ARGENT_PACKAGE_NAME}@${versionSpec} via bunx.`);
|
|
@@ -69,24 +66,19 @@ function createStartArgentRemoteSessionBuildFunction(ctx) {
|
|
|
69
66
|
parse: parseArgentToolServerState,
|
|
70
67
|
});
|
|
71
68
|
logger.info(`Argent tool-server is listening on port ${toolServerPort}.`);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
logger.info('Waiting for a public tunnel URL.');
|
|
79
|
-
const toolsUrl = await (0, remoteDeviceRunSession_1.waitForMatchInOutputAsync)({
|
|
80
|
-
process: cloudflared,
|
|
81
|
-
pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
|
|
82
|
-
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
83
|
-
description: 'cloudflared tunnel',
|
|
69
|
+
const toolsUrl = await (0, remoteDeviceRunSession_1.startNgrokTunnelAsync)({
|
|
70
|
+
port: toolServerPort,
|
|
71
|
+
subdomainPrefix: 'argent',
|
|
72
|
+
baseDomain: ngrokTunnelDomain,
|
|
73
|
+
authtoken: ngrokAuthtoken,
|
|
74
|
+
logger,
|
|
84
75
|
});
|
|
85
76
|
logger.info(`Tunnel is ready at ${toolsUrl}.`);
|
|
86
77
|
// serve-sim is iOS-only — Android sessions go without a preview URL.
|
|
87
78
|
let webPreviewUrl;
|
|
88
79
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
89
80
|
const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
|
|
81
|
+
baseDomain: ngrokTunnelDomain,
|
|
90
82
|
env,
|
|
91
83
|
logger,
|
|
92
84
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
@@ -52,6 +52,12 @@ function createStartIosSimulatorBuildFunction() {
|
|
|
52
52
|
deviceIdentifier: originalDeviceIdentifier,
|
|
53
53
|
env,
|
|
54
54
|
});
|
|
55
|
+
try {
|
|
56
|
+
await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid, env });
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
logger.warn({ err }, 'Failed to disable apsd in the Simulator.');
|
|
60
|
+
}
|
|
55
61
|
await IosSimulatorUtils_1.IosSimulatorUtils.waitForReadyAsync({ udid, env });
|
|
56
62
|
logger.info('');
|
|
57
63
|
const device = await IosSimulatorUtils_1.IosSimulatorUtils.getDeviceAsync({ udid, env });
|
|
@@ -76,6 +82,12 @@ function createStartIosSimulatorBuildFunction() {
|
|
|
76
82
|
deviceIdentifier: cloneDeviceName,
|
|
77
83
|
env,
|
|
78
84
|
});
|
|
85
|
+
try {
|
|
86
|
+
await IosSimulatorUtils_1.IosSimulatorUtils.disableApsdAsync({ udid: cloneUdid, env });
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
logger.warn({ err }, 'Failed to disable apsd in the Simulator.');
|
|
90
|
+
}
|
|
79
91
|
await IosSimulatorUtils_1.IosSimulatorUtils.waitForReadyAsync({
|
|
80
92
|
udid: cloneUdid,
|
|
81
93
|
env,
|
|
@@ -18,12 +18,12 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
|
18
18
|
supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
|
|
19
19
|
fn: async ({ logger }, { env }) => {
|
|
20
20
|
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
21
|
+
const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
|
|
21
22
|
logger.info('Starting serve-sim remote session.');
|
|
22
23
|
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
23
24
|
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
24
|
-
logger.info('Ensuring cloudflared is installed.');
|
|
25
|
-
await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
|
|
26
25
|
const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
|
|
26
|
+
baseDomain: ngrokTunnelDomain,
|
|
27
27
|
env,
|
|
28
28
|
logger,
|
|
29
29
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
@@ -2,9 +2,9 @@ import { bunyan } from '@expo/logger';
|
|
|
2
2
|
import { BuildStepEnv } from '@expo/steps';
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Catches all internal failures (logged + routed to Sentry); returns
|
|
6
|
+
* `{ skipped: true }` when analysis did not produce a report so callers can
|
|
7
|
+
* mark the build phase skipped.
|
|
8
8
|
*/
|
|
9
9
|
export declare function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, env, }: {
|
|
10
10
|
derivedDataPath: string;
|
|
@@ -13,7 +13,9 @@ export declare function parseAndReportXcactivitylog({ derivedDataPath, workspace
|
|
|
13
13
|
logger: bunyan;
|
|
14
14
|
proxyBaseUrl?: string;
|
|
15
15
|
env: BuildStepEnv;
|
|
16
|
-
}): Promise<
|
|
16
|
+
}): Promise<{
|
|
17
|
+
skipped: boolean;
|
|
18
|
+
}>;
|
|
17
19
|
declare const XcactivitylogStepSchemaZ: z.ZodObject<{
|
|
18
20
|
title: z.ZodOptional<z.ZodString>;
|
|
19
21
|
detailStepType: z.ZodOptional<z.ZodString>;
|
|
@@ -21,14 +21,40 @@ const XCLOGPARSER_DOWNLOAD_URL = 'https://storage.googleapis.com/turtle-v2/xclog
|
|
|
21
21
|
const XCLOGPARSER_DOWNLOAD_TIMEOUT_MS = 20_000;
|
|
22
22
|
const XCLOGPARSER_OUTPUT_FILENAME = 'xcactivitylog.json';
|
|
23
23
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
* Catches all internal failures (logged + routed to Sentry); returns
|
|
25
|
+
* `{ skipped: true }` when analysis did not produce a report so callers can
|
|
26
|
+
* mark the build phase skipped.
|
|
27
27
|
*/
|
|
28
28
|
async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, env, }) {
|
|
29
29
|
let tempDir;
|
|
30
|
-
let phase = '
|
|
30
|
+
let phase = 'checking_xcactivitylog_existence';
|
|
31
31
|
try {
|
|
32
|
+
const logsBuildDir = path_1.default.join(derivedDataPath, 'Logs', 'Build');
|
|
33
|
+
let buildLogEntries;
|
|
34
|
+
try {
|
|
35
|
+
buildLogEntries = await fs_extra_1.default.readdir(logsBuildDir);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (err?.code !== 'ENOENT') {
|
|
39
|
+
throw err;
|
|
40
|
+
}
|
|
41
|
+
buildLogEntries = [];
|
|
42
|
+
}
|
|
43
|
+
const hasActivityLog = buildLogEntries.some(entry => entry.endsWith('.xcactivitylog'));
|
|
44
|
+
if (!hasActivityLog) {
|
|
45
|
+
logger.info([
|
|
46
|
+
`Build performance analysis skipped: no .xcactivitylog files found at ${logsBuildDir}.`,
|
|
47
|
+
'',
|
|
48
|
+
'This typically happens when your project has a custom ios/Gymfile. To enable',
|
|
49
|
+
'build performance analysis, add the following lines to your Gymfile:',
|
|
50
|
+
'',
|
|
51
|
+
' derived_data_path("./build")',
|
|
52
|
+
' result_bundle(true)',
|
|
53
|
+
' result_bundle_path("./build/result-bundle.xcresult")',
|
|
54
|
+
].join('\n'));
|
|
55
|
+
return { skipped: true };
|
|
56
|
+
}
|
|
57
|
+
phase = 'creating_temp_directory';
|
|
32
58
|
tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'xclogparser-'));
|
|
33
59
|
phase = 'resolving_xclogparser';
|
|
34
60
|
const preinstalledVersion = await detectPreinstalledXclogparserVersion(env);
|
|
@@ -63,6 +89,7 @@ async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xcl
|
|
|
63
89
|
phase = 'parsing_xclogparser_output';
|
|
64
90
|
const data = XcactivitylogDataSchemaZ.parse(JSON.parse(await fs_extra_1.default.readFile(jsonOutputPath, 'utf8')));
|
|
65
91
|
logger.info(formatReport(data));
|
|
92
|
+
return { skipped: false };
|
|
66
93
|
}
|
|
67
94
|
catch (err) {
|
|
68
95
|
logger.info('Build performance analysis skipped.');
|
|
@@ -76,6 +103,7 @@ async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xcl
|
|
|
76
103
|
stdout: err?.stdout?.slice(-4000),
|
|
77
104
|
},
|
|
78
105
|
});
|
|
106
|
+
return { skipped: true };
|
|
79
107
|
}
|
|
80
108
|
finally {
|
|
81
109
|
if (tempDir) {
|
|
@@ -1,18 +1,15 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
|
-
import {
|
|
2
|
+
import { BuildStepEnv } from '@expo/steps';
|
|
3
3
|
import { CustomBuildContext } from '../../customBuildContext';
|
|
4
4
|
export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
|
|
5
|
+
export declare function getNgrokTunnelDomainOrThrow(env: BuildStepEnv): string;
|
|
6
|
+
export declare function getNgrokAuthtokenOrThrow(env: BuildStepEnv): string;
|
|
5
7
|
export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
|
|
6
8
|
ctx: CustomBuildContext;
|
|
7
9
|
deviceRunSessionId: string;
|
|
8
10
|
remoteConfig: Record<string, unknown>;
|
|
9
11
|
logger: bunyan;
|
|
10
12
|
}): Promise<void>;
|
|
11
|
-
export declare function ensureBrewPackageInstalledAsync({ name, env, logger, }: {
|
|
12
|
-
name: string;
|
|
13
|
-
env: BuildStepEnv;
|
|
14
|
-
logger: bunyan;
|
|
15
|
-
}): Promise<void>;
|
|
16
13
|
export type DetachedProcessHandle = {
|
|
17
14
|
getOutput: () => string;
|
|
18
15
|
};
|
|
@@ -22,7 +19,8 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
|
|
|
22
19
|
cwd?: string;
|
|
23
20
|
env: BuildStepEnv;
|
|
24
21
|
}): DetachedProcessHandle;
|
|
25
|
-
export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }: {
|
|
22
|
+
export declare function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }: {
|
|
23
|
+
baseDomain: string;
|
|
26
24
|
env: BuildStepEnv;
|
|
27
25
|
logger: bunyan;
|
|
28
26
|
timeoutMs: number;
|
|
@@ -30,17 +28,13 @@ export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }
|
|
|
30
28
|
previewUrl: string;
|
|
31
29
|
streamUrl: string;
|
|
32
30
|
}>;
|
|
33
|
-
export declare function
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
export declare function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, logger, }: {
|
|
32
|
+
port: number;
|
|
33
|
+
subdomainPrefix: string;
|
|
34
|
+
baseDomain: string;
|
|
35
|
+
authtoken: string;
|
|
36
36
|
logger: bunyan;
|
|
37
37
|
}): Promise<string>;
|
|
38
|
-
export declare function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }: {
|
|
39
|
-
process: DetachedProcessHandle;
|
|
40
|
-
pattern: RegExp;
|
|
41
|
-
timeoutMs: number;
|
|
42
|
-
description: string;
|
|
43
|
-
}): Promise<string>;
|
|
44
38
|
export declare function waitForFileAsync<T>({ filePath, timeoutMs, description, parse, }: {
|
|
45
39
|
filePath: string;
|
|
46
40
|
timeoutMs: number;
|
|
@@ -1,25 +1,56 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
39
|
exports.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
|
|
40
|
+
exports.getNgrokTunnelDomainOrThrow = getNgrokTunnelDomainOrThrow;
|
|
41
|
+
exports.getNgrokAuthtokenOrThrow = getNgrokAuthtokenOrThrow;
|
|
7
42
|
exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
|
|
8
|
-
exports.ensureBrewPackageInstalledAsync = ensureBrewPackageInstalledAsync;
|
|
9
43
|
exports.spawnDetached = spawnDetached;
|
|
10
44
|
exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
|
|
11
|
-
exports.
|
|
12
|
-
exports.waitForMatchInOutputAsync = waitForMatchInOutputAsync;
|
|
45
|
+
exports.startNgrokTunnelAsync = startNgrokTunnelAsync;
|
|
13
46
|
exports.waitForFileAsync = waitForFileAsync;
|
|
14
47
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
15
|
-
const steps_1 = require("@expo/steps");
|
|
16
48
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
49
|
+
const ngrok = __importStar(require("@ngrok/ngrok"));
|
|
17
50
|
const gql_tada_1 = require("gql.tada");
|
|
51
|
+
const node_crypto_1 = require("node:crypto");
|
|
18
52
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
19
|
-
const node_os_1 = __importDefault(require("node:os"));
|
|
20
53
|
const retry_1 = require("../../utils/retry");
|
|
21
|
-
const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
22
|
-
const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
|
|
23
54
|
const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
24
55
|
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
|
25
56
|
deviceRunSession {
|
|
@@ -37,11 +68,29 @@ function getDeviceRunSessionIdOrThrow(env) {
|
|
|
37
68
|
const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
|
|
38
69
|
if (!deviceRunSessionId) {
|
|
39
70
|
throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
|
|
40
|
-
'This step must run as part of a device run session
|
|
71
|
+
'This step must run as part of a device run session ' +
|
|
41
72
|
'which injects DEVICE_RUN_SESSION_ID into the job environment.');
|
|
42
73
|
}
|
|
43
74
|
return deviceRunSessionId;
|
|
44
75
|
}
|
|
76
|
+
function getNgrokTunnelDomainOrThrow(env) {
|
|
77
|
+
const baseDomain = env.EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN;
|
|
78
|
+
if (!baseDomain) {
|
|
79
|
+
throw new eas_build_job_1.SystemError('EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN is not set. ' +
|
|
80
|
+
'This step must run as part of a device run session ' +
|
|
81
|
+
'which injects EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN into the job environment.');
|
|
82
|
+
}
|
|
83
|
+
return baseDomain;
|
|
84
|
+
}
|
|
85
|
+
function getNgrokAuthtokenOrThrow(env) {
|
|
86
|
+
const authtoken = env.NGROK_AUTHTOKEN;
|
|
87
|
+
if (!authtoken) {
|
|
88
|
+
throw new eas_build_job_1.SystemError('NGROK_AUTHTOKEN is not set. ' +
|
|
89
|
+
'This step must run as part of a device run session ' +
|
|
90
|
+
'which injects NGROK_AUTHTOKEN into the job environment.');
|
|
91
|
+
}
|
|
92
|
+
return authtoken;
|
|
93
|
+
}
|
|
45
94
|
async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
|
|
46
95
|
logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
|
|
47
96
|
const result = await ctx.graphqlClient
|
|
@@ -51,9 +100,6 @@ async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteC
|
|
|
51
100
|
throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
|
|
52
101
|
}
|
|
53
102
|
}
|
|
54
|
-
async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
|
|
55
|
-
await (0, turtle_spawn_1.default)('bash', ['-c', `command -v ${name} >/dev/null 2>&1 || HOMEBREW_NO_AUTO_UPDATE=1 brew install ${name}`], { env, logger });
|
|
56
|
-
}
|
|
57
103
|
function spawnDetached({ command, args, cwd, env, }) {
|
|
58
104
|
const promise = (0, turtle_spawn_1.default)(command, args, {
|
|
59
105
|
cwd,
|
|
@@ -73,19 +119,23 @@ function spawnDetached({ command, args, cwd, env, }) {
|
|
|
73
119
|
promise.child.stderr?.on('data', appendChunk);
|
|
74
120
|
return { getOutput: () => output };
|
|
75
121
|
}
|
|
76
|
-
async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
|
|
122
|
+
async function startServeSimWithTunnelAsync({ baseDomain, env, logger, timeoutMs, }) {
|
|
77
123
|
logger.info('Launching serve-sim with tunnel.');
|
|
78
124
|
const serveSim = spawnDetached({
|
|
79
125
|
command: 'npx',
|
|
80
126
|
args: [
|
|
81
127
|
'serve-sim-szdziedzic@latest',
|
|
82
128
|
'--tunnel',
|
|
83
|
-
'--tunnel-
|
|
84
|
-
'
|
|
129
|
+
'--tunnel-provider',
|
|
130
|
+
'ngrok',
|
|
131
|
+
'--tunnel-domain',
|
|
132
|
+
baseDomain,
|
|
85
133
|
'--stream-max-dimension',
|
|
86
134
|
'1280',
|
|
87
135
|
'--stream-quality',
|
|
88
136
|
'0.55',
|
|
137
|
+
'--codec',
|
|
138
|
+
'webrtc',
|
|
89
139
|
],
|
|
90
140
|
env,
|
|
91
141
|
});
|
|
@@ -93,8 +143,8 @@ async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
|
|
|
93
143
|
const deadline = Date.now() + timeoutMs;
|
|
94
144
|
while (Date.now() < deadline) {
|
|
95
145
|
const output = serveSim.getOutput();
|
|
96
|
-
const previewUrl = matchLabeledUrl(output, 'Tunnel');
|
|
97
|
-
const streamUrl = matchLabeledUrl(output, 'Stream');
|
|
146
|
+
const previewUrl = matchLabeledUrl({ output, label: 'Tunnel', baseDomain });
|
|
147
|
+
const streamUrl = matchLabeledUrl({ output, label: 'Stream', baseDomain });
|
|
98
148
|
if (previewUrl && streamUrl) {
|
|
99
149
|
return { previewUrl, streamUrl };
|
|
100
150
|
}
|
|
@@ -102,59 +152,25 @@ async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
|
|
|
102
152
|
}
|
|
103
153
|
throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim to report Tunnel and Stream URLs. Last output:\n${serveSim.getOutput() || '<empty>'}`);
|
|
104
154
|
}
|
|
105
|
-
function matchLabeledUrl(
|
|
106
|
-
const labelPattern = new RegExp(`${label}:\\s*(
|
|
107
|
-
const match = labelPattern.exec(
|
|
155
|
+
function matchLabeledUrl({ output, label, baseDomain, }) {
|
|
156
|
+
const labelPattern = new RegExp(`${label}:\\s*(https:\\/\\/[a-z0-9-]+\\.${escapeRegExp(baseDomain)})`);
|
|
157
|
+
const match = labelPattern.exec(output);
|
|
108
158
|
return match ? match[1] : null;
|
|
109
159
|
}
|
|
110
|
-
async function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const downloadUrl = `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${cloudflaredArch}`;
|
|
120
|
-
logger.info(`Downloading cloudflared from ${downloadUrl} to ${CLOUDFLARED_LINUX_INSTALL_PATH}.`);
|
|
121
|
-
await (0, turtle_spawn_1.default)('sudo', ['curl', '-fsSL', '-o', CLOUDFLARED_LINUX_INSTALL_PATH, downloadUrl], {
|
|
122
|
-
env,
|
|
123
|
-
logger,
|
|
124
|
-
});
|
|
125
|
-
await (0, turtle_spawn_1.default)('sudo', ['chmod', '+x', CLOUDFLARED_LINUX_INSTALL_PATH], { env, logger });
|
|
126
|
-
// Return the absolute install path so the tunnel command works even when
|
|
127
|
-
// /usr/local/bin is not on the step's PATH.
|
|
128
|
-
return CLOUDFLARED_LINUX_INSTALL_PATH;
|
|
129
|
-
}
|
|
130
|
-
function cloudflaredLinuxArchForNodeArch(arch) {
|
|
131
|
-
if (arch === 'x64') {
|
|
132
|
-
return 'amd64';
|
|
133
|
-
}
|
|
134
|
-
if (arch === 'arm64') {
|
|
135
|
-
return 'arm64';
|
|
136
|
-
}
|
|
137
|
-
throw new eas_build_job_1.SystemError(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
|
|
138
|
-
}
|
|
139
|
-
async function isCommandAvailableAsync({ command, env, }) {
|
|
140
|
-
try {
|
|
141
|
-
await (0, turtle_spawn_1.default)('bash', ['-c', `command -v ${command}`], { env, ignoreStdio: true });
|
|
142
|
-
return true;
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return false;
|
|
160
|
+
async function startNgrokTunnelAsync({ port, subdomainPrefix, baseDomain, authtoken, logger, }) {
|
|
161
|
+
const domain = `${subdomainPrefix}-${(0, node_crypto_1.randomBytes)(8).toString('hex')}.${baseDomain}`;
|
|
162
|
+
logger.info(`Starting ngrok tunnel ${domain} -> http://localhost:${port}.`);
|
|
163
|
+
// Run the ngrok agent in-process via the SDK; it keeps the session alive until
|
|
164
|
+
// the process exits, and the step blocks forever to hold it open.
|
|
165
|
+
const listener = await ngrok.forward({ addr: port, authtoken, domain });
|
|
166
|
+
const url = listener.url();
|
|
167
|
+
if (!url) {
|
|
168
|
+
throw new eas_build_job_1.SystemError(`ngrok tunnel for ${domain} did not return a public URL.`);
|
|
146
169
|
}
|
|
170
|
+
return url;
|
|
147
171
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
while (Date.now() < deadline) {
|
|
151
|
-
const match = pattern.exec(process.getOutput());
|
|
152
|
-
if (match) {
|
|
153
|
-
return match[1] ?? match[0];
|
|
154
|
-
}
|
|
155
|
-
await (0, retry_1.sleepAsync)(1_000);
|
|
156
|
-
}
|
|
157
|
-
throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`);
|
|
172
|
+
function escapeRegExp(value) {
|
|
173
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
158
174
|
}
|
|
159
175
|
async function waitForFileAsync({ filePath, timeoutMs, description, parse, }) {
|
|
160
176
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -49,6 +49,10 @@ export declare namespace IosSimulatorUtils {
|
|
|
49
49
|
udid: IosSimulatorUuid;
|
|
50
50
|
env: NodeJS.ProcessEnv;
|
|
51
51
|
}): Promise<void>;
|
|
52
|
+
export function disableApsdAsync({ udid, env, }: {
|
|
53
|
+
udid: IosSimulatorUuid;
|
|
54
|
+
env: NodeJS.ProcessEnv;
|
|
55
|
+
}): Promise<void>;
|
|
52
56
|
export function collectLogsAsync({ deviceIdentifier, env, }: {
|
|
53
57
|
deviceIdentifier: IosSimulatorName | IosSimulatorUuid;
|
|
54
58
|
env: NodeJS.ProcessEnv;
|
|
@@ -49,8 +49,9 @@ var IosSimulatorUtils;
|
|
|
49
49
|
}
|
|
50
50
|
IosSimulatorUtils.startAsync = startAsync;
|
|
51
51
|
async function waitForReadyAsync({ udid, env, }) {
|
|
52
|
+
const readinessScreenshotPath = node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-simulator-readiness.png');
|
|
52
53
|
await (0, retry_1.retryAsync)(async () => {
|
|
53
|
-
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'screenshot',
|
|
54
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'io', udid, 'screenshot', readinessScreenshotPath], {
|
|
54
55
|
env,
|
|
55
56
|
});
|
|
56
57
|
}, {
|
|
@@ -60,6 +61,7 @@ var IosSimulatorUtils;
|
|
|
60
61
|
retryIntervalMs: 1_000,
|
|
61
62
|
},
|
|
62
63
|
});
|
|
64
|
+
await node_fs_1.default.promises.rm(readinessScreenshotPath, { force: true });
|
|
63
65
|
// Wait for data migration to complete before declaring the simulator ready
|
|
64
66
|
// Based on WebKit's approach: https://trac.webkit.org/changeset/231452/webkit
|
|
65
67
|
await (0, retry_1.retryAsync)(async () => {
|
|
@@ -75,6 +77,11 @@ var IosSimulatorUtils;
|
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
IosSimulatorUtils.waitForReadyAsync = waitForReadyAsync;
|
|
80
|
+
async function disableApsdAsync({ udid, env, }) {
|
|
81
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'disable', 'system/com.apple.apsd'], { env });
|
|
82
|
+
await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'bootout', 'system/com.apple.apsd'], { env });
|
|
83
|
+
}
|
|
84
|
+
IosSimulatorUtils.disableApsdAsync = disableApsdAsync;
|
|
78
85
|
async function collectLogsAsync({ deviceIdentifier, env, }) {
|
|
79
86
|
const outputDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'ios-simulator-logs-'));
|
|
80
87
|
const outputPath = node_path_1.default.join(outputDir, `${deviceIdentifier}.logarchive`);
|
package/dist/utils/artifacts.js
CHANGED
|
@@ -13,21 +13,15 @@ const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
|
13
13
|
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
14
14
|
const path_1 = __importDefault(require("path"));
|
|
15
15
|
const promise_limit_1 = __importDefault(require("promise-limit"));
|
|
16
|
-
const sentry_1 = require("../sentry");
|
|
17
16
|
class FindArtifactsError extends Error {
|
|
18
17
|
}
|
|
19
18
|
exports.FindArtifactsError = FindArtifactsError;
|
|
20
19
|
async function findArtifacts({ rootDir, patternOrPath, logger, }) {
|
|
21
|
-
const files = path_1.default.isAbsolute(patternOrPath)
|
|
20
|
+
const files = path_1.default.isAbsolute(patternOrPath) && !fast_glob_1.default.isDynamicPattern(patternOrPath)
|
|
22
21
|
? (await fs_extra_1.default.pathExists(patternOrPath))
|
|
23
22
|
? [patternOrPath]
|
|
24
23
|
: []
|
|
25
24
|
: await (0, fast_glob_1.default)(patternOrPath, { cwd: rootDir, onlyFiles: false });
|
|
26
|
-
await maybeReportAbsoluteGlobDryRunMismatchAsync({
|
|
27
|
-
rootDir,
|
|
28
|
-
patternOrPath,
|
|
29
|
-
files,
|
|
30
|
-
});
|
|
31
25
|
if (files.length === 0) {
|
|
32
26
|
if (fast_glob_1.default.isDynamicPattern(patternOrPath)) {
|
|
33
27
|
throw new FindArtifactsError(`There are no files matching pattern "${patternOrPath}"`);
|
|
@@ -50,60 +44,6 @@ async function findArtifacts({ rootDir, patternOrPath, logger, }) {
|
|
|
50
44
|
return path_1.default.join(rootDir, filePath);
|
|
51
45
|
});
|
|
52
46
|
}
|
|
53
|
-
async function findArtifactsWithAbsoluteGlobSupportDryRunAsync(rootDir, patternOrPath) {
|
|
54
|
-
return path_1.default.isAbsolute(patternOrPath) && !fast_glob_1.default.isDynamicPattern(patternOrPath)
|
|
55
|
-
? (await fs_extra_1.default.pathExists(patternOrPath))
|
|
56
|
-
? [patternOrPath]
|
|
57
|
-
: []
|
|
58
|
-
: await (0, fast_glob_1.default)(patternOrPath, { cwd: rootDir, onlyFiles: false });
|
|
59
|
-
}
|
|
60
|
-
async function maybeReportAbsoluteGlobDryRunMismatchAsync({ rootDir, patternOrPath, files, }) {
|
|
61
|
-
if (!path_1.default.isAbsolute(patternOrPath)) {
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
try {
|
|
65
|
-
const filesWithAbsoluteGlobSupport = await findArtifactsWithAbsoluteGlobSupportDryRunAsync(rootDir, patternOrPath);
|
|
66
|
-
if (areArtifactListsEqual(files, filesWithAbsoluteGlobSupport)) {
|
|
67
|
-
return;
|
|
68
|
-
}
|
|
69
|
-
sentry_1.Sentry.capture(new Error('findArtifacts output changed for an absolute path'), {
|
|
70
|
-
tags: {
|
|
71
|
-
source: 'find-artifacts',
|
|
72
|
-
reason: 'absolute_path',
|
|
73
|
-
},
|
|
74
|
-
extras: {
|
|
75
|
-
rootDir,
|
|
76
|
-
patternOrPath,
|
|
77
|
-
currentCount: files.length,
|
|
78
|
-
dryRunCount: filesWithAbsoluteGlobSupport.length,
|
|
79
|
-
currentSample: files.slice(0, 20),
|
|
80
|
-
dryRunSample: filesWithAbsoluteGlobSupport.slice(0, 20),
|
|
81
|
-
},
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
catch (err) {
|
|
85
|
-
sentry_1.Sentry.capture(err, {
|
|
86
|
-
tags: {
|
|
87
|
-
source: 'find-artifacts',
|
|
88
|
-
reason: 'absolute_path_dry_run_failed',
|
|
89
|
-
},
|
|
90
|
-
extras: {
|
|
91
|
-
rootDir,
|
|
92
|
-
patternOrPath,
|
|
93
|
-
currentCount: files.length,
|
|
94
|
-
currentSample: files.slice(0, 20),
|
|
95
|
-
},
|
|
96
|
-
});
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function areArtifactListsEqual(first, second) {
|
|
100
|
-
if (first.length !== second.length) {
|
|
101
|
-
return false;
|
|
102
|
-
}
|
|
103
|
-
const sortedFirst = [...first].sort();
|
|
104
|
-
const sortedSecond = [...second].sort();
|
|
105
|
-
return sortedFirst.every((artifactPath, index) => artifactPath === sortedSecond[index]);
|
|
106
|
-
}
|
|
107
47
|
async function logMissingFileError(artifactPath, buildLogger) {
|
|
108
48
|
let currentPath = artifactPath;
|
|
109
49
|
while (!(await fs_extra_1.default.pathExists(currentPath))) {
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.uploadEmbeddedBundleAsync = uploadEmbeddedBundleAsync;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
8
|
+
const logger_1 = require("@expo/logger");
|
|
9
|
+
const results_1 = require("@expo/results");
|
|
10
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
11
|
+
const os_1 = __importDefault(require("os"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const node_stream_zip_1 = __importDefault(require("node-stream-zip"));
|
|
14
|
+
const artifacts_1 = require("./artifacts");
|
|
15
|
+
const easCli_1 = require("./easCli");
|
|
16
|
+
const resolve_1 = require("../ios/resolve");
|
|
17
|
+
const expoUpdates_1 = require("./expoUpdates");
|
|
18
|
+
async function uploadEmbeddedBundleAsync(ctx) {
|
|
19
|
+
if (!(await (0, expoUpdates_1.isEASUpdateConfigured)(ctx))) {
|
|
20
|
+
ctx.markBuildPhaseSkipped();
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
const { platform } = ctx.job;
|
|
24
|
+
if (platform === eas_build_job_1.Platform.IOS && ctx.job.simulator) {
|
|
25
|
+
ctx.markBuildPhaseSkipped();
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const channel = ctx.job.updates?.channel;
|
|
29
|
+
if (!channel) {
|
|
30
|
+
ctx.logger.warn('Skipping embedded bundle upload: no channel configured for this build profile.');
|
|
31
|
+
ctx.markBuildPhaseHasWarnings();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const projectDir = ctx.getReactNativeProjectDirectory();
|
|
35
|
+
let archivePattern;
|
|
36
|
+
if (platform === eas_build_job_1.Platform.IOS) {
|
|
37
|
+
archivePattern = (0, resolve_1.resolveArtifactPath)(ctx);
|
|
38
|
+
}
|
|
39
|
+
else if (platform === eas_build_job_1.Platform.ANDROID) {
|
|
40
|
+
archivePattern =
|
|
41
|
+
ctx.job.applicationArchivePath ??
|
|
42
|
+
'android/app/build/outputs/**/*.{apk,aab}';
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
throw new Error(`Uploading embedded updates is not supported for the ${platform} platform.`);
|
|
46
|
+
}
|
|
47
|
+
const [archivePath] = await (0, artifacts_1.findArtifacts)({
|
|
48
|
+
rootDir: projectDir,
|
|
49
|
+
patternOrPath: archivePattern,
|
|
50
|
+
logger: null,
|
|
51
|
+
}).catch(() => []);
|
|
52
|
+
if (!archivePath) {
|
|
53
|
+
ctx.logger.warn('Skipping embedded bundle upload: build archive not found.');
|
|
54
|
+
ctx.markBuildPhaseHasWarnings();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const tmpDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'eas-embedded-bundle-'));
|
|
58
|
+
const bundleName = platform === eas_build_job_1.Platform.IOS ? 'main.jsbundle' : 'index.android.bundle';
|
|
59
|
+
const bundlePath = path_1.default.join(tmpDir, bundleName);
|
|
60
|
+
const manifestPath = path_1.default.join(tmpDir, 'app.manifest');
|
|
61
|
+
const zip = new node_stream_zip_1.default.async({ file: archivePath });
|
|
62
|
+
try {
|
|
63
|
+
const entries = Object.values(await zip.entries());
|
|
64
|
+
const bundleEntry = entries.find(e => platform === eas_build_job_1.Platform.IOS
|
|
65
|
+
? e.name.endsWith('/main.jsbundle')
|
|
66
|
+
: e.name.endsWith('assets/index.android.bundle'));
|
|
67
|
+
const manifestEntry = entries.find(e => platform === eas_build_job_1.Platform.IOS
|
|
68
|
+
? e.name.includes('EXUpdates.bundle/app.manifest')
|
|
69
|
+
: e.name.endsWith('assets/app.manifest'));
|
|
70
|
+
if (!bundleEntry || !manifestEntry) {
|
|
71
|
+
ctx.logger.warn('Skipping embedded bundle upload: bundle or manifest not found in archive.');
|
|
72
|
+
ctx.markBuildPhaseHasWarnings();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
await zip.extract(bundleEntry.name, bundlePath);
|
|
76
|
+
await zip.extract(manifestEntry.name, manifestPath);
|
|
77
|
+
const args = [
|
|
78
|
+
'update:embedded:upload',
|
|
79
|
+
'--platform',
|
|
80
|
+
platform,
|
|
81
|
+
'--bundle',
|
|
82
|
+
bundlePath,
|
|
83
|
+
'--manifest',
|
|
84
|
+
manifestPath,
|
|
85
|
+
'--channel',
|
|
86
|
+
channel,
|
|
87
|
+
'--non-interactive',
|
|
88
|
+
];
|
|
89
|
+
if (ctx.env.EAS_BUILD_ID) {
|
|
90
|
+
args.push('--build-id', ctx.env.EAS_BUILD_ID);
|
|
91
|
+
}
|
|
92
|
+
await (0, easCli_1.runEasCliCommand)({
|
|
93
|
+
args,
|
|
94
|
+
options: {
|
|
95
|
+
cwd: projectDir,
|
|
96
|
+
env: ctx.env,
|
|
97
|
+
logger: ctx.logger,
|
|
98
|
+
mode: logger_1.PipeMode.STDERR_ONLY_AS_STDOUT,
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
ctx.logger.warn({ err }, 'Failed to upload embedded bundle.');
|
|
104
|
+
ctx.markBuildPhaseHasWarnings();
|
|
105
|
+
}
|
|
106
|
+
finally {
|
|
107
|
+
await (0, results_1.asyncResult)(zip.close());
|
|
108
|
+
}
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "20.1.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -37,19 +37,20 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@expo/config": "55.0.10",
|
|
39
39
|
"@expo/config-plugins": "55.0.7",
|
|
40
|
-
"@expo/downloader": "
|
|
41
|
-
"@expo/eas-build-job": "
|
|
40
|
+
"@expo/downloader": "20.0.0",
|
|
41
|
+
"@expo/eas-build-job": "20.1.0",
|
|
42
42
|
"@expo/env": "^0.4.0",
|
|
43
|
-
"@expo/logger": "
|
|
43
|
+
"@expo/logger": "20.0.0",
|
|
44
44
|
"@expo/package-manager": "1.9.10",
|
|
45
45
|
"@expo/plist": "^0.2.0",
|
|
46
46
|
"@expo/results": "^1.0.0",
|
|
47
47
|
"@expo/spawn-async": "1.7.2",
|
|
48
|
-
"@expo/steps": "
|
|
49
|
-
"@expo/template-file": "
|
|
50
|
-
"@expo/turtle-spawn": "
|
|
48
|
+
"@expo/steps": "20.1.0",
|
|
49
|
+
"@expo/template-file": "20.0.0",
|
|
50
|
+
"@expo/turtle-spawn": "20.0.0",
|
|
51
51
|
"@expo/xcpretty": "^4.3.1",
|
|
52
52
|
"@google-cloud/storage": "^7.11.2",
|
|
53
|
+
"@ngrok/ngrok": "1.7.0",
|
|
53
54
|
"@sentry/node": "7.77.0",
|
|
54
55
|
"@urql/core": "^6.0.1",
|
|
55
56
|
"bplist-parser": "0.3.2",
|
|
@@ -99,5 +100,5 @@
|
|
|
99
100
|
"typescript": "^5.5.4",
|
|
100
101
|
"uuid": "^9.0.1"
|
|
101
102
|
},
|
|
102
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "33225d2b73a34db5cbdfeaa537aedb6c6b4a84af"
|
|
103
104
|
}
|