@expo/build-tools 18.12.0 → 18.12.3
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/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/sentry.d.ts +19 -0
- package/dist/sentry.js +102 -0
- package/dist/steps/easFunctions.js +2 -0
- package/dist/steps/functions/startAgentDeviceRemoteSession.js +42 -91
- package/dist/steps/functions/startServeSimRemoteSession.d.ts +3 -0
- package/dist/steps/functions/startServeSimRemoteSession.js +45 -0
- package/dist/steps/utils/ios/xcactivitylog.d.ts +3 -5
- package/dist/steps/utils/ios/xcactivitylog.js +11 -6
- package/dist/steps/utils/remoteDeviceRunSession.d.ts +32 -0
- package/dist/steps/utils/remoteDeviceRunSession.js +93 -0
- package/package.json +3 -2
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -39,7 +39,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
39
39
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
40
|
};
|
|
41
41
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
-
exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.GCSLoggerStream = exports.GCS = exports.Builders = void 0;
|
|
42
|
+
exports.Sentry = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.GCSLoggerStream = exports.GCS = exports.Builders = void 0;
|
|
43
43
|
const Builders = __importStar(require("./builders"));
|
|
44
44
|
exports.Builders = Builders;
|
|
45
45
|
const LoggerStream_1 = __importDefault(require("./gcs/LoggerStream"));
|
|
@@ -57,3 +57,5 @@ var hooks_1 = require("./utils/hooks");
|
|
|
57
57
|
Object.defineProperty(exports, "Hook", { enumerable: true, get: function () { return hooks_1.Hook; } });
|
|
58
58
|
Object.defineProperty(exports, "runHookIfPresent", { enumerable: true, get: function () { return hooks_1.runHookIfPresent; } });
|
|
59
59
|
__exportStar(require("./generic"), exports);
|
|
60
|
+
var sentry_1 = require("./sentry");
|
|
61
|
+
Object.defineProperty(exports, "Sentry", { enumerable: true, get: function () { return sentry_1.Sentry; } });
|
package/dist/sentry.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as sentryNode from '@sentry/node';
|
|
2
|
+
type CaptureOptions = {
|
|
3
|
+
tags?: Record<string, string>;
|
|
4
|
+
extras?: Record<string, unknown>;
|
|
5
|
+
level?: sentryNode.SeverityLevel;
|
|
6
|
+
};
|
|
7
|
+
type SentryAPI = {
|
|
8
|
+
setup(opts: {
|
|
9
|
+
dsn: string | null;
|
|
10
|
+
environment: string;
|
|
11
|
+
tags?: Record<string, string>;
|
|
12
|
+
}): void;
|
|
13
|
+
capture(msg: string, options?: CaptureOptions): void;
|
|
14
|
+
capture(msg: string, err: Error | undefined, options?: CaptureOptions): void;
|
|
15
|
+
capture(err: Error, options?: CaptureOptions): void;
|
|
16
|
+
flush(timeoutMs?: number): Promise<boolean>;
|
|
17
|
+
};
|
|
18
|
+
export declare const Sentry: SentryAPI;
|
|
19
|
+
export {};
|
package/dist/sentry.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
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
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.Sentry = void 0;
|
|
37
|
+
const sentryNode = __importStar(require("@sentry/node"));
|
|
38
|
+
exports.Sentry = {
|
|
39
|
+
setup({ dsn, environment, tags }) {
|
|
40
|
+
if (dsn) {
|
|
41
|
+
sentryNode.init({
|
|
42
|
+
dsn,
|
|
43
|
+
environment,
|
|
44
|
+
...(tags ? { initialScope: { tags } } : {}),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
capture(arg1, arg2, arg3) {
|
|
49
|
+
let msg;
|
|
50
|
+
let err;
|
|
51
|
+
let options = {};
|
|
52
|
+
if (arg1 instanceof Error) {
|
|
53
|
+
err = arg1;
|
|
54
|
+
options = arg2 ?? {};
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
msg = arg1;
|
|
58
|
+
if (arg3 !== undefined) {
|
|
59
|
+
// 3-arg form: arg2 unambiguously means err (null/undefined → no err)
|
|
60
|
+
options = arg3;
|
|
61
|
+
if (arg2 !== undefined && arg2 !== null) {
|
|
62
|
+
err = arg2 instanceof Error ? arg2 : new Error(String(arg2));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
else if (arg2 instanceof Error) {
|
|
66
|
+
err = arg2;
|
|
67
|
+
}
|
|
68
|
+
else if (arg2 !== undefined && arg2 !== null) {
|
|
69
|
+
// 2-arg form: non-Error object → options; primitive → coerced err
|
|
70
|
+
if (typeof arg2 === 'object') {
|
|
71
|
+
options = arg2;
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
err = new Error(String(arg2));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
sentryNode.withScope(scope => {
|
|
79
|
+
if (options.tags) {
|
|
80
|
+
scope.setTags(options.tags);
|
|
81
|
+
}
|
|
82
|
+
if (options.extras) {
|
|
83
|
+
scope.setExtras(options.extras);
|
|
84
|
+
}
|
|
85
|
+
if (options.level) {
|
|
86
|
+
scope.setLevel(options.level);
|
|
87
|
+
}
|
|
88
|
+
if (err) {
|
|
89
|
+
if (msg && err.message !== msg) {
|
|
90
|
+
scope.setExtra('message', msg);
|
|
91
|
+
}
|
|
92
|
+
sentryNode.captureException(err);
|
|
93
|
+
}
|
|
94
|
+
else if (msg) {
|
|
95
|
+
sentryNode.captureMessage(msg);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
},
|
|
99
|
+
flush(timeoutMs = 2000) {
|
|
100
|
+
return sentryNode.flush(timeoutMs);
|
|
101
|
+
},
|
|
102
|
+
};
|
|
@@ -41,6 +41,7 @@ const startAgentDeviceRemoteSession_1 = require("./functions/startAgentDeviceRem
|
|
|
41
41
|
const startAndroidEmulator_1 = require("./functions/startAndroidEmulator");
|
|
42
42
|
const startCuttlefishDevice_1 = require("./functions/startCuttlefishDevice");
|
|
43
43
|
const startIosSimulator_1 = require("./functions/startIosSimulator");
|
|
44
|
+
const startServeSimRemoteSession_1 = require("./functions/startServeSimRemoteSession");
|
|
44
45
|
const uploadArtifact_1 = require("./functions/uploadArtifact");
|
|
45
46
|
const uploadToAsc_1 = require("./functions/uploadToAsc");
|
|
46
47
|
const useNpmToken_1 = require("./functions/useNpmToken");
|
|
@@ -79,6 +80,7 @@ function getEasFunctions(ctx) {
|
|
|
79
80
|
(0, startAndroidEmulator_1.createStartAndroidEmulatorBuildFunction)(),
|
|
80
81
|
(0, startCuttlefishDevice_1.createStartCuttlefishDeviceBuildFunction)(),
|
|
81
82
|
(0, startIosSimulator_1.createStartIosSimulatorBuildFunction)(),
|
|
83
|
+
(0, startServeSimRemoteSession_1.createStartServeSimRemoteSessionBuildFunction)(ctx),
|
|
82
84
|
(0, installMaestro_1.createInstallMaestroBuildFunction)(),
|
|
83
85
|
(0, installPods_1.createInstallPodsBuildFunction)(),
|
|
84
86
|
(0, sendSlackMessage_1.createSendSlackMessageFunction)(),
|
|
@@ -4,37 +4,20 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createStartAgentDeviceRemoteSessionBuildFunction = createStartAgentDeviceRemoteSessionBuildFunction;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
7
8
|
const steps_1 = require("@expo/steps");
|
|
8
9
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
|
-
const gql_tada_1 = require("gql.tada");
|
|
10
|
-
const node_child_process_1 = __importDefault(require("node:child_process"));
|
|
11
10
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
11
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
12
|
const node_path_1 = __importDefault(require("node:path"));
|
|
14
13
|
const retry_1 = require("../../utils/retry");
|
|
15
|
-
const
|
|
14
|
+
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
16
15
|
const AGENT_DEVICE_REPO_URL = 'https://github.com/callstackincubator/agent-device.git';
|
|
17
16
|
const SRC_DIR = '/tmp/agent-device-src';
|
|
18
|
-
const RUN_DIR = '/tmp/agent-device';
|
|
19
|
-
const DAEMON_LOG = node_path_1.default.join(RUN_DIR, 'daemon.log');
|
|
20
|
-
const TUNNEL_LOG = node_path_1.default.join(RUN_DIR, 'cloudflared.log');
|
|
21
17
|
const DAEMON_JSON_PATH = node_path_1.default.join(node_os_1.default.homedir(), '.agent-device', 'daemon.json');
|
|
22
18
|
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
23
19
|
const CLOUDFLARED_LINUX_INSTALL_PATH = '/usr/local/bin/cloudflared';
|
|
24
20
|
const STARTUP_TIMEOUT_MS = 60_000;
|
|
25
|
-
const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
26
|
-
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
|
27
|
-
deviceRunSession {
|
|
28
|
-
startDeviceRunSession(
|
|
29
|
-
deviceRunSessionId: $deviceRunSessionId
|
|
30
|
-
remoteConfig: $remoteConfig
|
|
31
|
-
) {
|
|
32
|
-
id
|
|
33
|
-
status
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
`);
|
|
38
21
|
function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
39
22
|
return new steps_1.BuildFunction({
|
|
40
23
|
namespace: 'eas',
|
|
@@ -52,17 +35,10 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
52
35
|
// Fail fast before any expensive setup if the orchestrator-injected
|
|
53
36
|
// DEVICE_RUN_SESSION_ID env var is missing — without it we cannot
|
|
54
37
|
// report the remote config back to the API server.
|
|
55
|
-
const deviceRunSessionId = env
|
|
56
|
-
if (!deviceRunSessionId) {
|
|
57
|
-
throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
|
|
58
|
-
'This step must run as part of a device run session created by the API server, ' +
|
|
59
|
-
'which injects DEVICE_RUN_SESSION_ID into the job environment.');
|
|
60
|
-
}
|
|
38
|
+
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
61
39
|
const packageVersion = inputs.package_version.value;
|
|
62
40
|
const { runtimePlatform } = global;
|
|
63
41
|
logger.info(`Starting agent-device remote session (version: ${packageVersion ?? 'latest'}, runtime: ${runtimePlatform}).`);
|
|
64
|
-
logger.info(`Preparing runtime directory at ${RUN_DIR}.`);
|
|
65
|
-
await node_fs_1.default.promises.mkdir(RUN_DIR, { recursive: true });
|
|
66
42
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
67
43
|
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
68
44
|
await (0, turtle_spawn_1.default)('sudo', ['xcode-select', '-s', XCODE_DEVELOPER_DIR], { env, logger });
|
|
@@ -83,13 +59,12 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
83
59
|
env,
|
|
84
60
|
logger,
|
|
85
61
|
});
|
|
86
|
-
logger.info(
|
|
87
|
-
|
|
88
|
-
command: 'bun
|
|
62
|
+
logger.info('Launching agent-device daemon.');
|
|
63
|
+
(0, remoteDeviceRunSession_1.spawnDetached)({
|
|
64
|
+
command: 'bun',
|
|
65
|
+
args: ['run', 'src/daemon.ts'],
|
|
89
66
|
cwd: SRC_DIR,
|
|
90
|
-
logFile: DAEMON_LOG,
|
|
91
67
|
env: { ...env, AGENT_DEVICE_DAEMON_SERVER_MODE: 'http' },
|
|
92
|
-
logger,
|
|
93
68
|
});
|
|
94
69
|
logger.info(`Waiting for daemon credentials at ${DAEMON_JSON_PATH}.`);
|
|
95
70
|
await waitForFileAsync({
|
|
@@ -99,31 +74,42 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
99
74
|
});
|
|
100
75
|
const { port: daemonPort, token: daemonToken } = readDaemonInfo(DAEMON_JSON_PATH);
|
|
101
76
|
logger.info(`Daemon is listening on port ${daemonPort}; loaded auth token.`);
|
|
102
|
-
logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort}
|
|
103
|
-
|
|
104
|
-
command:
|
|
105
|
-
|
|
77
|
+
logger.info(`Starting cloudflared tunnel to http://localhost:${daemonPort}.`);
|
|
78
|
+
const cloudflared = (0, remoteDeviceRunSession_1.spawnDetached)({
|
|
79
|
+
command: cloudflaredCommand,
|
|
80
|
+
args: ['tunnel', '--url', `http://localhost:${daemonPort}`],
|
|
106
81
|
env,
|
|
107
|
-
logger,
|
|
108
82
|
});
|
|
109
83
|
logger.info('Waiting for a public tunnel URL.');
|
|
110
|
-
const
|
|
111
|
-
|
|
84
|
+
const agentDeviceRemoteSessionUrl = await waitForMatchInOutputAsync({
|
|
85
|
+
process: cloudflared,
|
|
112
86
|
pattern: /https:\/\/[a-z0-9-]+\.trycloudflare\.com/,
|
|
113
87
|
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
114
88
|
description: 'cloudflared tunnel',
|
|
115
89
|
});
|
|
116
|
-
logger.info(`Tunnel is ready at ${
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
90
|
+
logger.info(`Tunnel is ready at ${agentDeviceRemoteSessionUrl}.`);
|
|
91
|
+
// serve-sim is iOS-only — only launch it (and report a webPreviewUrl)
|
|
92
|
+
// on Darwin. Android sessions go without a preview URL.
|
|
93
|
+
let webPreviewUrl;
|
|
94
|
+
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
95
|
+
const { previewUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
|
|
96
|
+
env,
|
|
97
|
+
logger,
|
|
98
|
+
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
99
|
+
});
|
|
100
|
+
webPreviewUrl = previewUrl;
|
|
101
|
+
logger.info(`Web preview URL: ${webPreviewUrl}`);
|
|
126
102
|
}
|
|
103
|
+
await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
|
|
104
|
+
ctx,
|
|
105
|
+
deviceRunSessionId,
|
|
106
|
+
remoteConfig: {
|
|
107
|
+
agentDeviceRemoteSessionUrl,
|
|
108
|
+
agentDeviceRemoteSessionToken: daemonToken,
|
|
109
|
+
...(webPreviewUrl ? { webPreviewUrl } : {}),
|
|
110
|
+
},
|
|
111
|
+
logger,
|
|
112
|
+
});
|
|
127
113
|
logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
|
|
128
114
|
// Keep the turtle job alive so the daemon and tunnel stay reachable
|
|
129
115
|
// until stopDeviceRunSession cancels the run.
|
|
@@ -133,7 +119,7 @@ function createStartAgentDeviceRemoteSessionBuildFunction(ctx) {
|
|
|
133
119
|
}
|
|
134
120
|
async function ensureCloudflaredInstalledAsync({ runtimePlatform, env, logger, }) {
|
|
135
121
|
if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
136
|
-
await ensureBrewPackageInstalledAsync({ name: 'cloudflared', env, logger });
|
|
122
|
+
await (0, remoteDeviceRunSession_1.ensureBrewPackageInstalledAsync)({ name: 'cloudflared', env, logger });
|
|
137
123
|
return 'cloudflared';
|
|
138
124
|
}
|
|
139
125
|
if (await isCommandAvailableAsync({ command: 'cloudflared', env })) {
|
|
@@ -158,10 +144,7 @@ function cloudflaredLinuxArchForNodeArch(arch) {
|
|
|
158
144
|
if (arch === 'arm64') {
|
|
159
145
|
return 'arm64';
|
|
160
146
|
}
|
|
161
|
-
throw new
|
|
162
|
-
}
|
|
163
|
-
async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
|
|
164
|
-
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 });
|
|
147
|
+
throw new eas_build_job_1.SystemError(`Unsupported architecture for cloudflared on Linux: "${arch}". Expected "x64" or "arm64".`);
|
|
165
148
|
}
|
|
166
149
|
async function isCommandAvailableAsync({ command, env, }) {
|
|
167
150
|
try {
|
|
@@ -179,40 +162,16 @@ async function cloneAgentDeviceAsync({ packageVersion, env, logger, }) {
|
|
|
179
162
|
logger,
|
|
180
163
|
});
|
|
181
164
|
}
|
|
182
|
-
async function
|
|
183
|
-
// Launch the process fully detached so this function returns immediately and
|
|
184
|
-
// the grandchild survives the step. Stdio goes to a log file so the daemon
|
|
185
|
-
// output can be polled, and we unref so Node doesn't wait on it.
|
|
186
|
-
const fd = node_fs_1.default.openSync(logFile, 'a');
|
|
187
|
-
try {
|
|
188
|
-
const child = node_child_process_1.default.spawn('bash', ['-c', command], {
|
|
189
|
-
cwd,
|
|
190
|
-
env,
|
|
191
|
-
detached: true,
|
|
192
|
-
stdio: ['ignore', fd, fd],
|
|
193
|
-
});
|
|
194
|
-
if (!child.pid) {
|
|
195
|
-
throw new Error(`Failed to spawn detached process: ${command}`);
|
|
196
|
-
}
|
|
197
|
-
child.unref();
|
|
198
|
-
logger.info(`Started detached process (pid ${child.pid}).`);
|
|
199
|
-
}
|
|
200
|
-
finally {
|
|
201
|
-
node_fs_1.default.closeSync(fd);
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
async function waitForMatchInLogAsync({ logFile, pattern, timeoutMs, description, }) {
|
|
165
|
+
async function waitForMatchInOutputAsync({ process, pattern, timeoutMs, description, }) {
|
|
205
166
|
const deadline = Date.now() + timeoutMs;
|
|
206
167
|
while (Date.now() < deadline) {
|
|
207
|
-
const
|
|
208
|
-
const match = pattern.exec(content);
|
|
168
|
+
const match = pattern.exec(process.getOutput());
|
|
209
169
|
if (match) {
|
|
210
170
|
return match[1] ?? match[0];
|
|
211
171
|
}
|
|
212
172
|
await (0, retry_1.sleepAsync)(1_000);
|
|
213
173
|
}
|
|
214
|
-
|
|
215
|
-
throw new Error(`Timed out waiting for ${description} to start. Last log contents:\n${tail || '<empty>'}`);
|
|
174
|
+
throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to start. Last output:\n${process.getOutput() || '<empty>'}`);
|
|
216
175
|
}
|
|
217
176
|
async function waitForFileAsync({ filePath, timeoutMs, description, }) {
|
|
218
177
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -226,15 +185,7 @@ async function waitForFileAsync({ filePath, timeoutMs, description, }) {
|
|
|
226
185
|
}
|
|
227
186
|
await (0, retry_1.sleepAsync)(1_000);
|
|
228
187
|
}
|
|
229
|
-
throw new
|
|
230
|
-
}
|
|
231
|
-
async function readFileOrEmptyAsync(filePath) {
|
|
232
|
-
try {
|
|
233
|
-
return await node_fs_1.default.promises.readFile(filePath, 'utf8');
|
|
234
|
-
}
|
|
235
|
-
catch {
|
|
236
|
-
return '';
|
|
237
|
-
}
|
|
188
|
+
throw new eas_build_job_1.SystemError(`Timed out waiting for ${description} to write ${filePath}.`);
|
|
238
189
|
}
|
|
239
190
|
function readDaemonInfo(filePath) {
|
|
240
191
|
const raw = node_fs_1.default.readFileSync(filePath, 'utf8');
|
|
@@ -243,7 +194,7 @@ function readDaemonInfo(filePath) {
|
|
|
243
194
|
typeof parsed !== 'object' ||
|
|
244
195
|
typeof parsed.httpPort !== 'number' ||
|
|
245
196
|
typeof parsed.token !== 'string') {
|
|
246
|
-
throw new
|
|
197
|
+
throw new eas_build_job_1.SystemError(`Expected ${filePath} to contain { "httpPort": <number>, "token": "..." }.`);
|
|
247
198
|
}
|
|
248
199
|
const { httpPort, token } = parsed;
|
|
249
200
|
return { port: httpPort, token };
|
|
@@ -0,0 +1,45 @@
|
|
|
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.createStartServeSimRemoteSessionBuildFunction = createStartServeSimRemoteSessionBuildFunction;
|
|
7
|
+
const steps_1 = require("@expo/steps");
|
|
8
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
|
+
const remoteDeviceRunSession_1 = require("../utils/remoteDeviceRunSession");
|
|
10
|
+
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
11
|
+
const STARTUP_TIMEOUT_MS = 60_000;
|
|
12
|
+
function createStartServeSimRemoteSessionBuildFunction(ctx) {
|
|
13
|
+
return new steps_1.BuildFunction({
|
|
14
|
+
namespace: 'eas',
|
|
15
|
+
id: 'start_serve_sim_remote_session',
|
|
16
|
+
name: 'Start serve-sim remote session',
|
|
17
|
+
__metricsId: 'eas/start_serve_sim_remote_session',
|
|
18
|
+
supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
|
|
19
|
+
fn: async ({ logger }, { env }) => {
|
|
20
|
+
const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
|
|
21
|
+
logger.info('Starting serve-sim remote session.');
|
|
22
|
+
logger.info(`Selecting Xcode developer directory: ${XCODE_DEVELOPER_DIR}.`);
|
|
23
|
+
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
|
+
const { previewUrl, streamUrl } = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)({
|
|
27
|
+
env,
|
|
28
|
+
logger,
|
|
29
|
+
timeoutMs: STARTUP_TIMEOUT_MS,
|
|
30
|
+
});
|
|
31
|
+
logger.info(`Preview URL: ${previewUrl}`);
|
|
32
|
+
logger.info(`Stream URL: ${streamUrl}`);
|
|
33
|
+
await (0, remoteDeviceRunSession_1.uploadRemoteSessionConfigAsync)({
|
|
34
|
+
ctx,
|
|
35
|
+
deviceRunSessionId,
|
|
36
|
+
remoteConfig: { previewUrl, streamUrl },
|
|
37
|
+
logger,
|
|
38
|
+
});
|
|
39
|
+
logger.info('Remote session is live. Keeping the job alive until the session is stopped.');
|
|
40
|
+
// Keep the turtle job alive so the serve-sim tunnel stays reachable
|
|
41
|
+
// until stopDeviceRunSession cancels the run.
|
|
42
|
+
await new Promise(() => { });
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { bunyan } from '@expo/logger';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* Can be called from both the step-based flow (BuildFunction) and the
|
|
8
|
-
* traditional builder flow (runBuildPhase).
|
|
4
|
+
* Never throws — best-effort observability that does not affect build status.
|
|
5
|
+
* Failures route to Sentry via `Sentry.capture` for engineering triage;
|
|
6
|
+
* users see only a generic skip message.
|
|
9
7
|
*/
|
|
10
8
|
export declare function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion, logger, proxyBaseUrl, }: {
|
|
11
9
|
derivedDataPath: string;
|
|
@@ -15,33 +15,38 @@ const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
|
15
15
|
const os_1 = __importDefault(require("os"));
|
|
16
16
|
const path_1 = __importDefault(require("path"));
|
|
17
17
|
const zod_1 = require("zod");
|
|
18
|
+
const sentry_1 = require("../../../sentry");
|
|
18
19
|
const DEFAULT_XCLOGPARSER_VERSION = 'v0.2.47';
|
|
19
20
|
const XCLOGPARSER_DOWNLOAD_URL = 'https://storage.googleapis.com/turtle-v2/xclogparser';
|
|
20
21
|
const XCLOGPARSER_DOWNLOAD_TIMEOUT_MS = 20_000;
|
|
21
22
|
const XCLOGPARSER_OUTPUT_FILENAME = 'xcactivitylog.json';
|
|
22
23
|
/**
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* Can be called from both the step-based flow (BuildFunction) and the
|
|
27
|
-
* traditional builder flow (runBuildPhase).
|
|
24
|
+
* Never throws — best-effort observability that does not affect build status.
|
|
25
|
+
* Failures route to Sentry via `Sentry.capture` for engineering triage;
|
|
26
|
+
* users see only a generic skip message.
|
|
28
27
|
*/
|
|
29
28
|
async function parseAndReportXcactivitylog({ derivedDataPath, workspacePath, xclogparserVersion = DEFAULT_XCLOGPARSER_VERSION, logger, proxyBaseUrl, }) {
|
|
30
29
|
let tempDir;
|
|
30
|
+
let phase = 'creating_temp_directory';
|
|
31
31
|
try {
|
|
32
32
|
tempDir = await fs_extra_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'xclogparser-'));
|
|
33
|
+
phase = 'downloading_xclogparser';
|
|
33
34
|
const xclogparserPath = await downloadXclogparser(tempDir, xclogparserVersion, logger, proxyBaseUrl);
|
|
35
|
+
phase = 'running_xclogparser';
|
|
34
36
|
const jsonOutputPath = await runXclogparser({
|
|
35
37
|
binaryPath: xclogparserPath,
|
|
36
38
|
derivedDataPath,
|
|
37
39
|
workspacePath,
|
|
38
40
|
outputDir: tempDir,
|
|
39
41
|
});
|
|
42
|
+
phase = 'parsing_xclogparser_output';
|
|
40
43
|
const data = XcactivitylogDataSchemaZ.parse(JSON.parse(await fs_extra_1.default.readFile(jsonOutputPath, 'utf8')));
|
|
41
44
|
logger.info(formatReport(data));
|
|
42
45
|
}
|
|
43
46
|
catch (err) {
|
|
44
|
-
logger.
|
|
47
|
+
logger.info('Build performance analysis skipped.');
|
|
48
|
+
const msg = `Build performance analysis failed during "${phase}"`;
|
|
49
|
+
sentry_1.Sentry.capture(msg, err, { tags: { phase } });
|
|
45
50
|
}
|
|
46
51
|
finally {
|
|
47
52
|
if (tempDir) {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { bunyan } from '@expo/logger';
|
|
2
|
+
import { BuildStepEnv } from '@expo/steps';
|
|
3
|
+
import { CustomBuildContext } from '../../customBuildContext';
|
|
4
|
+
export declare function getDeviceRunSessionIdOrThrow(env: BuildStepEnv): string;
|
|
5
|
+
export declare function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }: {
|
|
6
|
+
ctx: CustomBuildContext;
|
|
7
|
+
deviceRunSessionId: string;
|
|
8
|
+
remoteConfig: Record<string, unknown>;
|
|
9
|
+
logger: bunyan;
|
|
10
|
+
}): Promise<void>;
|
|
11
|
+
export declare function ensureBrewPackageInstalledAsync({ name, env, logger, }: {
|
|
12
|
+
name: string;
|
|
13
|
+
env: BuildStepEnv;
|
|
14
|
+
logger: bunyan;
|
|
15
|
+
}): Promise<void>;
|
|
16
|
+
export type DetachedProcessHandle = {
|
|
17
|
+
getOutput: () => string;
|
|
18
|
+
};
|
|
19
|
+
export declare function spawnDetached({ command, args, cwd, env, }: {
|
|
20
|
+
command: string;
|
|
21
|
+
args: string[];
|
|
22
|
+
cwd?: string;
|
|
23
|
+
env: BuildStepEnv;
|
|
24
|
+
}): DetachedProcessHandle;
|
|
25
|
+
export declare function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }: {
|
|
26
|
+
env: BuildStepEnv;
|
|
27
|
+
logger: bunyan;
|
|
28
|
+
timeoutMs: number;
|
|
29
|
+
}): Promise<{
|
|
30
|
+
previewUrl: string;
|
|
31
|
+
streamUrl: string;
|
|
32
|
+
}>;
|
|
@@ -0,0 +1,93 @@
|
|
|
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.getDeviceRunSessionIdOrThrow = getDeviceRunSessionIdOrThrow;
|
|
7
|
+
exports.uploadRemoteSessionConfigAsync = uploadRemoteSessionConfigAsync;
|
|
8
|
+
exports.ensureBrewPackageInstalledAsync = ensureBrewPackageInstalledAsync;
|
|
9
|
+
exports.spawnDetached = spawnDetached;
|
|
10
|
+
exports.startServeSimWithTunnelAsync = startServeSimWithTunnelAsync;
|
|
11
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
12
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
13
|
+
const gql_tada_1 = require("gql.tada");
|
|
14
|
+
const retry_1 = require("../../utils/retry");
|
|
15
|
+
const TRYCLOUDFLARE_URL_PATTERN = /https:\/\/[a-z0-9-]+\.trycloudflare\.com/;
|
|
16
|
+
const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
17
|
+
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
|
18
|
+
deviceRunSession {
|
|
19
|
+
startDeviceRunSession(
|
|
20
|
+
deviceRunSessionId: $deviceRunSessionId
|
|
21
|
+
remoteConfig: $remoteConfig
|
|
22
|
+
) {
|
|
23
|
+
id
|
|
24
|
+
status
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`);
|
|
29
|
+
function getDeviceRunSessionIdOrThrow(env) {
|
|
30
|
+
const deviceRunSessionId = env.DEVICE_RUN_SESSION_ID;
|
|
31
|
+
if (!deviceRunSessionId) {
|
|
32
|
+
throw new eas_build_job_1.SystemError('DEVICE_RUN_SESSION_ID is not set. ' +
|
|
33
|
+
'This step must run as part of a device run session created by the API server, ' +
|
|
34
|
+
'which injects DEVICE_RUN_SESSION_ID into the job environment.');
|
|
35
|
+
}
|
|
36
|
+
return deviceRunSessionId;
|
|
37
|
+
}
|
|
38
|
+
async function uploadRemoteSessionConfigAsync({ ctx, deviceRunSessionId, remoteConfig, logger, }) {
|
|
39
|
+
logger.info(`Reporting remote config to the API server (device run session: ${deviceRunSessionId}).`);
|
|
40
|
+
const result = await ctx.graphqlClient
|
|
41
|
+
.mutation(START_DEVICE_RUN_SESSION_MUTATION, { deviceRunSessionId, remoteConfig })
|
|
42
|
+
.toPromise();
|
|
43
|
+
if (result.error) {
|
|
44
|
+
throw new eas_build_job_1.SystemError(`Failed to start device run session ${deviceRunSessionId}: ${result.error.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function ensureBrewPackageInstalledAsync({ name, env, logger, }) {
|
|
48
|
+
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 });
|
|
49
|
+
}
|
|
50
|
+
function spawnDetached({ command, args, cwd, env, }) {
|
|
51
|
+
const promise = (0, turtle_spawn_1.default)(command, args, {
|
|
52
|
+
cwd,
|
|
53
|
+
env,
|
|
54
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
55
|
+
detached: true,
|
|
56
|
+
});
|
|
57
|
+
// We don't await the process — it should outlive this step. Failures show
|
|
58
|
+
// up in the captured output; suppress unhandled rejections here.
|
|
59
|
+
promise.catch(() => { });
|
|
60
|
+
promise.child.unref();
|
|
61
|
+
let output = '';
|
|
62
|
+
const appendChunk = (chunk) => {
|
|
63
|
+
output += chunk.toString();
|
|
64
|
+
};
|
|
65
|
+
promise.child.stdout?.on('data', appendChunk);
|
|
66
|
+
promise.child.stderr?.on('data', appendChunk);
|
|
67
|
+
return { getOutput: () => output };
|
|
68
|
+
}
|
|
69
|
+
async function startServeSimWithTunnelAsync({ env, logger, timeoutMs, }) {
|
|
70
|
+
logger.info('Launching serve-sim with tunnel.');
|
|
71
|
+
const serveSim = spawnDetached({
|
|
72
|
+
command: 'npx',
|
|
73
|
+
args: ['serve-sim-szdziedzic@latest', '--tunnel'],
|
|
74
|
+
env,
|
|
75
|
+
});
|
|
76
|
+
logger.info('Waiting for serve-sim to report tunnel and stream URLs.');
|
|
77
|
+
const deadline = Date.now() + timeoutMs;
|
|
78
|
+
while (Date.now() < deadline) {
|
|
79
|
+
const output = serveSim.getOutput();
|
|
80
|
+
const previewUrl = matchLabeledUrl(output, 'Tunnel');
|
|
81
|
+
const streamUrl = matchLabeledUrl(output, 'Stream');
|
|
82
|
+
if (previewUrl && streamUrl) {
|
|
83
|
+
return { previewUrl, streamUrl };
|
|
84
|
+
}
|
|
85
|
+
await (0, retry_1.sleepAsync)(1_000);
|
|
86
|
+
}
|
|
87
|
+
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>'}`);
|
|
88
|
+
}
|
|
89
|
+
function matchLabeledUrl(content, label) {
|
|
90
|
+
const labelPattern = new RegExp(`${label}:\\s*(${TRYCLOUDFLARE_URL_PATTERN.source})`);
|
|
91
|
+
const match = labelPattern.exec(content);
|
|
92
|
+
return match ? match[1] : null;
|
|
93
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "18.12.
|
|
3
|
+
"version": "18.12.3",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"@expo/turtle-spawn": "18.5.0",
|
|
51
51
|
"@expo/xcpretty": "^4.3.1",
|
|
52
52
|
"@google-cloud/storage": "^7.11.2",
|
|
53
|
+
"@sentry/node": "7.77.0",
|
|
53
54
|
"@urql/core": "^6.0.1",
|
|
54
55
|
"bplist-parser": "0.3.2",
|
|
55
56
|
"fast-glob": "^3.3.2",
|
|
@@ -98,5 +99,5 @@
|
|
|
98
99
|
"typescript": "^5.5.4",
|
|
99
100
|
"uuid": "^9.0.1"
|
|
100
101
|
},
|
|
101
|
-
"gitHead": "
|
|
102
|
+
"gitHead": "985cd9d52f4de333cf563cc7fe92f4e2d1e1c8e5"
|
|
102
103
|
}
|