@expo/build-tools 21.7.1 → 22.0.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/common.js +4 -0
- package/dist/builders/ios.js +11 -6
- package/dist/steps/functions/installMaestro.js +84 -32
- package/dist/steps/functions/maestroBackend.d.ts +11 -0
- package/dist/steps/functions/maestroBackend.js +14 -0
- package/dist/steps/functions/maestroTests.js +36 -0
- package/dist/steps/functions/startAndroidEmulator.js +14 -1
- package/dist/utils/AndroidEmulatorUtils.d.ts +3 -1
- package/dist/utils/AndroidEmulatorUtils.js +23 -2
- package/dist/utils/sourceMaps.d.ts +5 -0
- package/dist/utils/sourceMaps.js +105 -0
- package/package.json +8 -8
package/dist/builders/common.js
CHANGED
|
@@ -5,6 +5,7 @@ const eas_build_job_1 = require("@expo/eas-build-job");
|
|
|
5
5
|
const xcodeBuildLogs_1 = require("../ios/xcodeBuildLogs");
|
|
6
6
|
const artifacts_1 = require("../utils/artifacts");
|
|
7
7
|
const hooks_1 = require("../utils/hooks");
|
|
8
|
+
const sourceMaps_1 = require("../utils/sourceMaps");
|
|
8
9
|
async function runBuilderWithHooksAsync(ctx, builderAsync) {
|
|
9
10
|
try {
|
|
10
11
|
let buildSuccess = true;
|
|
@@ -35,6 +36,9 @@ async function runBuilderWithHooksAsync(ctx, builderAsync) {
|
|
|
35
36
|
});
|
|
36
37
|
}
|
|
37
38
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.UPLOAD_BUILD_ARTIFACTS, async () => {
|
|
39
|
+
if (buildSuccess) {
|
|
40
|
+
await (0, sourceMaps_1.maybeUploadSourceMapAsync)(ctx);
|
|
41
|
+
}
|
|
38
42
|
await (0, artifacts_1.maybeFindAndUploadBuildArtifacts)(ctx, {
|
|
39
43
|
logger: ctx.logger,
|
|
40
44
|
});
|
package/dist/builders/ios.js
CHANGED
|
@@ -30,6 +30,7 @@ const expoUpdatesEmbedded_1 = require("../utils/expoUpdatesEmbedded");
|
|
|
30
30
|
const hooks_1 = require("../utils/hooks");
|
|
31
31
|
const prepareBuildExecutable_1 = require("../utils/prepareBuildExecutable");
|
|
32
32
|
const processes_1 = require("../utils/processes");
|
|
33
|
+
const sourceMaps_1 = require("../utils/sourceMaps");
|
|
33
34
|
const INSTALL_PODS_WARN_TIMEOUT_MS = 15 * 60 * 1000; // 15 minutes
|
|
34
35
|
const INSTALL_PODS_KILL_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
35
36
|
class InstallPodsTimeoutError extends Error {
|
|
@@ -151,19 +152,23 @@ async function buildInnerAsync(ctx, jobHooksRef) {
|
|
|
151
152
|
fastlaneResult = await ctx.runBuildPhase(eas_build_job_1.BuildPhase.RUN_FASTLANE, async () => {
|
|
152
153
|
const scheme = (0, resolve_1.resolveScheme)(ctx);
|
|
153
154
|
const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
|
|
155
|
+
const sourceMapPath = (0, sourceMaps_1.isSourceMapUploadEnabled)(ctx)
|
|
156
|
+
? await (0, sourceMaps_1.resolveIosSourceMapPathAsync)(ctx)
|
|
157
|
+
: null;
|
|
154
158
|
return await (0, fastlane_1.runFastlaneGym)(ctx, {
|
|
155
159
|
credentials,
|
|
156
160
|
scheme,
|
|
157
161
|
buildConfiguration,
|
|
158
162
|
entitlements,
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
163
|
+
extraEnv: {
|
|
164
|
+
...(resolvedExpoUpdatesRuntimeVersion?.runtimeVersion
|
|
165
|
+
? {
|
|
162
166
|
EXPO_UPDATES_FINGERPRINT_OVERRIDE: resolvedExpoUpdatesRuntimeVersion?.runtimeVersion,
|
|
163
167
|
EXPO_UPDATES_WORKFLOW_OVERRIDE: ctx.job.type,
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
: null),
|
|
168
|
+
}
|
|
169
|
+
: null),
|
|
170
|
+
...(sourceMapPath ? { SOURCEMAP_FILE: sourceMapPath } : null),
|
|
171
|
+
},
|
|
167
172
|
});
|
|
168
173
|
});
|
|
169
174
|
}
|
|
@@ -11,6 +11,7 @@ const assert_1 = __importDefault(require("assert"));
|
|
|
11
11
|
const fs_1 = __importDefault(require("fs"));
|
|
12
12
|
const os_1 = __importDefault(require("os"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
|
+
const maestroBackend_1 = require("./maestroBackend");
|
|
14
15
|
const datadog_1 = require("../../datadog");
|
|
15
16
|
function createInstallMaestroBuildFunction() {
|
|
16
17
|
return new steps_1.BuildFunction({
|
|
@@ -24,6 +25,11 @@ function createInstallMaestroBuildFunction() {
|
|
|
24
25
|
required: false,
|
|
25
26
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
26
27
|
}),
|
|
28
|
+
steps_1.BuildStepInput.createProvider({
|
|
29
|
+
id: 'backend',
|
|
30
|
+
required: false,
|
|
31
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
32
|
+
}),
|
|
27
33
|
],
|
|
28
34
|
outputProviders: [
|
|
29
35
|
steps_1.BuildStepOutput.createProvider({
|
|
@@ -32,35 +38,43 @@ function createInstallMaestroBuildFunction() {
|
|
|
32
38
|
}),
|
|
33
39
|
],
|
|
34
40
|
fn: async ({ logger, global }, { inputs, env, outputs }) => {
|
|
35
|
-
const
|
|
36
|
-
|
|
41
|
+
const backend = (0, maestroBackend_1.resolveMaestroBackend)({
|
|
42
|
+
input: inputs.backend.value,
|
|
43
|
+
env,
|
|
44
|
+
});
|
|
45
|
+
const requestedVersion = inputs.maestro_version.value;
|
|
46
|
+
const { value: currentMaestroVersion } = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
|
|
37
47
|
// When not running in EAS Build VM, do not modify local environment.
|
|
38
48
|
if (env.EAS_BUILD_RUNNER !== 'eas-build') {
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
49
|
+
const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
|
|
50
|
+
const needsToInstallIdb = backend === 'maestro' && !(await isIdbInstalled({ env }));
|
|
51
|
+
if (needsToInstallJava) {
|
|
42
52
|
logger.warn('It seems Java is not installed. It is required to run Maestro. If the job fails, this may be the reason.');
|
|
43
53
|
logger.info('');
|
|
44
54
|
}
|
|
45
|
-
if (
|
|
55
|
+
if (needsToInstallIdb) {
|
|
46
56
|
logger.warn('It seems IDB is not installed. Maestro requires it to run flows on iOS Simulator. If the job fails, this may be the reason.');
|
|
47
57
|
logger.info('');
|
|
48
58
|
}
|
|
49
59
|
if (!currentMaestroVersion) {
|
|
50
|
-
logger.warn(
|
|
60
|
+
logger.warn(`It seems ${backend} is not installed. Please install it manually and rerun the job.`);
|
|
51
61
|
logger.info('');
|
|
52
62
|
}
|
|
53
63
|
// Guide is helpful in these two cases, it doesn't mention Java.
|
|
54
|
-
if (
|
|
64
|
+
if (backend === 'maestro' && (needsToInstallIdb || !currentMaestroVersion)) {
|
|
55
65
|
logger.warn('For more info, check out Maestro installation guide: https://maestro.mobile.dev/getting-started/installing-maestro');
|
|
56
66
|
}
|
|
67
|
+
else if (backend === 'maestro-runner' && !currentMaestroVersion) {
|
|
68
|
+
logger.warn('For more info, check out maestro-runner installation guide: https://github.com/devicelab-dev/maestro-runner#install');
|
|
69
|
+
}
|
|
57
70
|
if (currentMaestroVersion) {
|
|
58
71
|
outputs.maestro_version.set(currentMaestroVersion);
|
|
59
|
-
logger.info(
|
|
72
|
+
logger.info(`${backend} ${currentMaestroVersion} is ready.`);
|
|
60
73
|
}
|
|
61
74
|
return;
|
|
62
75
|
}
|
|
63
|
-
|
|
76
|
+
const needsToInstallJava = backend === 'maestro' && !(await isJavaInstalled({ env }));
|
|
77
|
+
if (needsToInstallJava) {
|
|
64
78
|
if (global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
|
|
65
79
|
logger.info('Installing Java');
|
|
66
80
|
await installJavaFromGcs({ logger, env });
|
|
@@ -73,45 +87,83 @@ function createInstallMaestroBuildFunction() {
|
|
|
73
87
|
}
|
|
74
88
|
}
|
|
75
89
|
// IDB is only a requirement on macOS.
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
const needsToInstallIdb = backend === 'maestro' &&
|
|
91
|
+
global.runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN &&
|
|
92
|
+
!(await isIdbInstalled({ env }));
|
|
93
|
+
if (needsToInstallIdb) {
|
|
78
94
|
logger.info('Installing IDB');
|
|
79
95
|
await installIdbFromBrew({ logger, env });
|
|
80
96
|
}
|
|
81
97
|
// Skip installing if the input sets a specific Maestro version to install
|
|
82
98
|
// and it is already installed which happens when developing on a local computer.
|
|
83
99
|
if (!currentMaestroVersion ||
|
|
84
|
-
(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
100
|
+
(requestedVersion && requestedVersion !== currentMaestroVersion)) {
|
|
101
|
+
switch (backend) {
|
|
102
|
+
case 'maestro':
|
|
103
|
+
await installMaestro({ version: requestedVersion, global, logger, env });
|
|
104
|
+
break;
|
|
105
|
+
case 'maestro-runner':
|
|
106
|
+
await installMaestroRunner({ version: requestedVersion, global, logger, env });
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
91
109
|
}
|
|
92
|
-
const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env }));
|
|
110
|
+
const maestroVersionResult = await (0, results_1.asyncResult)(getMaestroVersion({ env, backend }));
|
|
93
111
|
if (!maestroVersionResult.ok) {
|
|
94
112
|
logger.error(maestroVersionResult.reason, 'Failed to get Maestro version.');
|
|
95
|
-
throw new Error(
|
|
113
|
+
throw new Error(`Failed to ensure ${backend} is installed.`);
|
|
96
114
|
}
|
|
97
|
-
logger.info(
|
|
115
|
+
logger.info(`${backend} ${maestroVersionResult.value} is ready.`);
|
|
98
116
|
outputs.maestro_version.set(maestroVersionResult.value);
|
|
99
117
|
datadog_1.Datadog.distribution('eas.maestro.install', 1, {
|
|
100
118
|
maestro_version: maestroVersionResult.value,
|
|
119
|
+
maestro_backend: backend,
|
|
101
120
|
});
|
|
102
121
|
},
|
|
103
122
|
});
|
|
104
123
|
}
|
|
105
|
-
async function getMaestroVersion({ env }) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
124
|
+
async function getMaestroVersion({ env, backend, }) {
|
|
125
|
+
switch (backend) {
|
|
126
|
+
case 'maestro': {
|
|
127
|
+
const { stdout } = await (0, turtle_spawn_1.default)('maestro', ['--version'], { stdio: 'pipe', env });
|
|
128
|
+
// `maestro --version` can print an analytics notice to stdout before the version,
|
|
129
|
+
// e.g. "Anonymous analytics enabled. To opt out, set MAESTRO_CLI_NO_ANALYTICS...\n2.0.10".
|
|
130
|
+
// Take the last version-looking token: the real version is printed after the notice.
|
|
131
|
+
const versions = stdout.match(/\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?/g);
|
|
132
|
+
return versions?.at(-1) ?? stdout.trim();
|
|
133
|
+
}
|
|
134
|
+
case 'maestro-runner': {
|
|
135
|
+
const { stdout } = await (0, turtle_spawn_1.default)('maestro-runner', ['--version'], { stdio: 'pipe', env });
|
|
136
|
+
// maestro-runner prints build information after its version. The Go runtime version in
|
|
137
|
+
// that output is also semver-shaped, so read only the prefixed runner version.
|
|
138
|
+
return /^maestro-runner\s+(\S+)/m.exec(stdout)?.[1] ?? stdout.trim();
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
async function installMaestroRunner({ global, version, logger, env, }) {
|
|
143
|
+
logger.info('Fetching maestro-runner install script');
|
|
144
|
+
const tempDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'install_maestro_runner'));
|
|
145
|
+
try {
|
|
146
|
+
const installMaestroRunnerScriptResponse = await fetch('https://open.devicelab.dev/install/maestro-runner');
|
|
147
|
+
const installMaestroRunnerScript = await installMaestroRunnerScriptResponse.text();
|
|
148
|
+
const scriptPath = path_1.default.join(tempDirectory, 'install_maestro_runner.sh');
|
|
149
|
+
await fs_1.default.promises.writeFile(scriptPath, installMaestroRunnerScript, { mode: 0o777 });
|
|
150
|
+
logger.info('Installing maestro-runner');
|
|
151
|
+
(0, assert_1.default)(env.HOME, 'Failed to infer directory to install maestro-runner in: $HOME environment variable is empty.');
|
|
152
|
+
await (0, turtle_spawn_1.default)(scriptPath, version && version !== 'latest' ? ['--version', version] : [], {
|
|
153
|
+
logger,
|
|
154
|
+
env,
|
|
155
|
+
});
|
|
156
|
+
const binDir = path_1.default.join(env.HOME, '.maestro-runner', 'bin');
|
|
157
|
+
global.updateEnv({
|
|
158
|
+
...global.env,
|
|
159
|
+
PATH: `${global.env.PATH}:${binDir}`,
|
|
160
|
+
});
|
|
161
|
+
env.PATH = `${env.PATH}:${binDir}`;
|
|
162
|
+
process.env.PATH = `${process.env.PATH}:${binDir}`;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
await fs_1.default.promises.rm(tempDirectory, { force: true, recursive: true });
|
|
166
|
+
}
|
|
115
167
|
}
|
|
116
168
|
async function installMaestro({ global, version, logger, env, }) {
|
|
117
169
|
logger.info('Fetching install script');
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { BuildStepEnv } from '@expo/steps';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
export declare const MaestroBackendSchema: z.ZodDefault<z.ZodEnum<{
|
|
4
|
+
maestro: "maestro";
|
|
5
|
+
"maestro-runner": "maestro-runner";
|
|
6
|
+
}>>;
|
|
7
|
+
export type MaestroBackend = z.output<typeof MaestroBackendSchema>;
|
|
8
|
+
export declare function resolveMaestroBackend({ input, env, }: {
|
|
9
|
+
input: unknown;
|
|
10
|
+
env: BuildStepEnv;
|
|
11
|
+
}): MaestroBackend;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MaestroBackendSchema = void 0;
|
|
4
|
+
exports.resolveMaestroBackend = resolveMaestroBackend;
|
|
5
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
exports.MaestroBackendSchema = zod_1.z.enum(['maestro', 'maestro-runner']).default('maestro');
|
|
8
|
+
function resolveMaestroBackend({ input, env, }) {
|
|
9
|
+
const result = exports.MaestroBackendSchema.safeParse(input || env.EAS_MAESTRO_BACKEND || undefined);
|
|
10
|
+
if (!result.success) {
|
|
11
|
+
throw new eas_build_job_1.UserError('ERR_MAESTRO_INVALID_INPUT', 'backend and EAS_MAESTRO_BACKEND must be either "maestro" or "maestro-runner".', { cause: result.error });
|
|
12
|
+
}
|
|
13
|
+
return result.data;
|
|
14
|
+
}
|
|
@@ -18,6 +18,7 @@ const retry_1 = require("../../utils/retry");
|
|
|
18
18
|
const FlowPathSchema = zod_1.z.array(zod_1.z.string().min(1)).min(1);
|
|
19
19
|
const RetriesSchema = zod_1.z.number().int().min(0).default(0);
|
|
20
20
|
const ShardsSchema = zod_1.z.number().int().min(1).optional();
|
|
21
|
+
const AndroidConnectionModeSchema = zod_1.z.enum(['adb', 'dadb']).default('adb');
|
|
21
22
|
function parseInput(schema, value, message) {
|
|
22
23
|
const result = schema.safeParse(value);
|
|
23
24
|
if (!result.success) {
|
|
@@ -109,6 +110,11 @@ function createMaestroTestsBuildFunction(ctx) {
|
|
|
109
110
|
required: false,
|
|
110
111
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
111
112
|
}),
|
|
113
|
+
steps_1.BuildStepInput.createProvider({
|
|
114
|
+
id: 'android_connection_mode',
|
|
115
|
+
required: false,
|
|
116
|
+
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
|
|
117
|
+
}),
|
|
112
118
|
],
|
|
113
119
|
outputProviders: [
|
|
114
120
|
steps_1.BuildStepOutput.createProvider({ id: 'junit_report_directory', required: true }),
|
|
@@ -153,6 +159,9 @@ function createMaestroTestsBuildFunction(ctx) {
|
|
|
153
159
|
const flowPaths = parseInput(FlowPathSchema, inputs.flow_path.value, 'flow_path must be a non-empty array of non-empty strings.');
|
|
154
160
|
const retries = parseInput(RetriesSchema, inputs.retries.value, 'retries must be a non-negative integer.');
|
|
155
161
|
const shards = parseInput(ShardsSchema, inputs.shards.value, 'shards must be a positive integer.');
|
|
162
|
+
const androidConnectionMode = parseInput(AndroidConnectionModeSchema, inputs.android_connection_mode.value ||
|
|
163
|
+
env.EAS_MAESTRO_ANDROID_CONNECTION_MODE ||
|
|
164
|
+
undefined, 'android_connection_mode and EAS_MAESTRO_ANDROID_CONNECTION_MODE must be either "adb" or "dadb".');
|
|
156
165
|
const retryFailedOnly = inputs.retry_failed_only.value;
|
|
157
166
|
try {
|
|
158
167
|
await promises_1.default.mkdir(junitReportDirectory, { recursive: true });
|
|
@@ -176,6 +185,33 @@ function createMaestroTestsBuildFunction(ctx) {
|
|
|
176
185
|
let lastAttemptExitCode = null;
|
|
177
186
|
const harvested = [];
|
|
178
187
|
const totalAttempts = retries + 1;
|
|
188
|
+
if (platform === 'android' && androidConnectionMode === 'dadb') {
|
|
189
|
+
try {
|
|
190
|
+
const adbOverrideDirectoryPath = await promises_1.default.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'maestro-tests-adb-override-'));
|
|
191
|
+
await promises_1.default.writeFile(path_1.default.join(adbOverrideDirectoryPath, 'adb'), '#!/bin/sh\nexit 1\n', {
|
|
192
|
+
mode: 0o755,
|
|
193
|
+
});
|
|
194
|
+
// DADB starts an ADB server when it can find an adb binary. Stop the existing
|
|
195
|
+
// server first, then make only the Maestro process find the failing shim.
|
|
196
|
+
try {
|
|
197
|
+
await (0, turtle_spawn_1.default)('adb', ['kill-server'], { env: { ...spawnEnv }, logger, signal });
|
|
198
|
+
logger.info('Using a direct DADB connection for Android Maestro tests after stopping the ADB server.');
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
logger.warn({ err }, 'Using a direct DADB connection for Android Maestro tests, but failed to stop the ADB server.');
|
|
202
|
+
}
|
|
203
|
+
spawnEnv.PATH = spawnEnv.PATH
|
|
204
|
+
? `${adbOverrideDirectoryPath}${path_1.default.delimiter}${spawnEnv.PATH}`
|
|
205
|
+
: adbOverrideDirectoryPath;
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
// Intentionally skip cleanup because the worker is disposable.
|
|
209
|
+
throw new eas_build_job_1.SystemError('Failed to enable direct DADB connection for Maestro', {
|
|
210
|
+
cause: err,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
// Do not restart ADB or remove the override. This keeps Maestro in direct DADB mode.
|
|
214
|
+
}
|
|
179
215
|
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
180
216
|
const outputPath = outputFormat === 'junit'
|
|
181
217
|
? path_1.default.join(junitReportDirectory, `${platform}-maestro-junit-attempt-${attempt}.xml`)
|
|
@@ -8,6 +8,9 @@ const eas_build_job_1 = require("@expo/eas-build-job");
|
|
|
8
8
|
const results_1 = require("@expo/results");
|
|
9
9
|
const steps_1 = require("@expo/steps");
|
|
10
10
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
11
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
13
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
11
14
|
const AndroidEmulatorUtils_1 = require("../../utils/AndroidEmulatorUtils");
|
|
12
15
|
const retry_1 = require("../../utils/retry");
|
|
13
16
|
const ANDROID_STARTUP_ATTEMPT_TIMEOUT_MS = [60_000, 120_000, 180_000];
|
|
@@ -43,7 +46,15 @@ function createStartAndroidEmulatorBuildFunction() {
|
|
|
43
46
|
allowedValueTypeName: steps_1.BuildStepInputValueTypeName.NUMBER,
|
|
44
47
|
}),
|
|
45
48
|
],
|
|
46
|
-
|
|
49
|
+
outputProviders: [
|
|
50
|
+
steps_1.BuildStepOutput.createProvider({
|
|
51
|
+
id: 'logcat_directory',
|
|
52
|
+
required: true,
|
|
53
|
+
}),
|
|
54
|
+
],
|
|
55
|
+
fn: async ({ logger }, { inputs, outputs, env }) => {
|
|
56
|
+
const logcatDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-android-emulator-logcat-'));
|
|
57
|
+
outputs.logcat_directory.set(logcatDirectory);
|
|
47
58
|
if (env.EAS_NO_EMULATOR_HOST_SUPPORT_CHECK !== '1') {
|
|
48
59
|
await assertAndroidEmulatorHostSupportAsync({ env });
|
|
49
60
|
}
|
|
@@ -103,6 +114,7 @@ function createStartAndroidEmulatorBuildFunction() {
|
|
|
103
114
|
const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
104
115
|
deviceName,
|
|
105
116
|
env,
|
|
117
|
+
logcatDirectory,
|
|
106
118
|
});
|
|
107
119
|
attemptSerialId = startResult.serialId;
|
|
108
120
|
await AndroidEmulatorUtils_1.AndroidEmulatorUtils.waitForReadyAsync({
|
|
@@ -184,6 +196,7 @@ function createStartAndroidEmulatorBuildFunction() {
|
|
|
184
196
|
const startResult = await AndroidEmulatorUtils_1.AndroidEmulatorUtils.startAsync({
|
|
185
197
|
deviceName: cloneIdentifier,
|
|
186
198
|
env,
|
|
199
|
+
logcatDirectory,
|
|
187
200
|
});
|
|
188
201
|
cloneSerialId = startResult.serialId;
|
|
189
202
|
logger.info('Waiting for emulator to become ready');
|
|
@@ -34,12 +34,14 @@ export declare namespace AndroidEmulatorUtils {
|
|
|
34
34
|
env: NodeJS.ProcessEnv;
|
|
35
35
|
logger: bunyan;
|
|
36
36
|
}): Promise<void>;
|
|
37
|
-
function startAsync({ deviceName, env, }: {
|
|
37
|
+
function startAsync({ deviceName, env, logcatDirectory, }: {
|
|
38
38
|
deviceName: AndroidVirtualDeviceName;
|
|
39
39
|
env: NodeJS.ProcessEnv;
|
|
40
|
+
logcatDirectory: string;
|
|
40
41
|
}): Promise<{
|
|
41
42
|
emulatorPromise: SpawnPromise<SpawnResult>;
|
|
42
43
|
serialId: AndroidDeviceSerialId;
|
|
44
|
+
logcatOutputPath: string;
|
|
43
45
|
}>;
|
|
44
46
|
function waitForReadyAsync({ serialId, env, timeoutMs, logger, }: {
|
|
45
47
|
serialId: AndroidDeviceSerialId;
|
|
@@ -4,10 +4,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.AndroidEmulatorUtils = void 0;
|
|
7
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
7
8
|
const results_1 = require("@expo/results");
|
|
8
9
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
9
10
|
const assert_1 = __importDefault(require("assert"));
|
|
10
11
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
11
13
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
12
14
|
const node_os_1 = __importDefault(require("node:os"));
|
|
13
15
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -212,13 +214,32 @@ var AndroidEmulatorUtils;
|
|
|
212
214
|
}
|
|
213
215
|
}
|
|
214
216
|
AndroidEmulatorUtils.cloneAsync = cloneAsync;
|
|
215
|
-
async function startAsync({ deviceName, env, }) {
|
|
217
|
+
async function startAsync({ deviceName, env, logcatDirectory, }) {
|
|
218
|
+
let logcatOutputPath;
|
|
219
|
+
try {
|
|
220
|
+
await node_fs_1.default.promises.mkdir(logcatDirectory, { recursive: true });
|
|
221
|
+
const safeDeviceName = deviceName.replace(/[^a-zA-Z0-9_.-]/g, '_');
|
|
222
|
+
const timestamp = Math.floor(Date.now() / 1000)
|
|
223
|
+
.toString(16)
|
|
224
|
+
.padStart(8, '0');
|
|
225
|
+
logcatOutputPath = node_path_1.default.join(logcatDirectory, `${safeDeviceName}-${timestamp}-${(0, node_crypto_1.randomBytes)(2).toString('hex')}.log`);
|
|
226
|
+
await node_fs_1.default.promises.writeFile(logcatOutputPath, '');
|
|
227
|
+
}
|
|
228
|
+
catch (err) {
|
|
229
|
+
throw new eas_build_job_1.SystemError(`Failed to prepare Android emulator logcat output for ${deviceName}.`, {
|
|
230
|
+
cause: err,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
216
233
|
const emulatorPromise = (0, turtle_spawn_1.default)(`${process.env.ANDROID_HOME}/emulator/emulator`, [
|
|
217
234
|
'-no-window',
|
|
218
235
|
'-no-boot-anim',
|
|
219
236
|
'-writable-system',
|
|
220
237
|
'-noaudio',
|
|
221
238
|
'-no-snapshot-save',
|
|
239
|
+
'-logcat',
|
|
240
|
+
'*:v',
|
|
241
|
+
'-logcat-output',
|
|
242
|
+
logcatOutputPath,
|
|
222
243
|
'-avd',
|
|
223
244
|
deviceName,
|
|
224
245
|
'-accel',
|
|
@@ -255,7 +276,7 @@ var AndroidEmulatorUtils;
|
|
|
255
276
|
});
|
|
256
277
|
// We don't want to await the SpawnPromise here.
|
|
257
278
|
// eslint-disable-next-line @typescript-eslint/return-await
|
|
258
|
-
return { emulatorPromise, serialId };
|
|
279
|
+
return { emulatorPromise, serialId, logcatOutputPath };
|
|
259
280
|
}
|
|
260
281
|
AndroidEmulatorUtils.startAsync = startAsync;
|
|
261
282
|
async function waitForReadyAsync({ serialId, env, timeoutMs = 3 * 60 * 1_000, logger, }) {
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { BuildJob } from '@expo/eas-build-job';
|
|
2
|
+
import { BuildContext } from '../context';
|
|
3
|
+
export declare function isSourceMapUploadEnabled(ctx: BuildContext<BuildJob>): boolean;
|
|
4
|
+
export declare function resolveIosSourceMapPathAsync(ctx: BuildContext<BuildJob>): Promise<string>;
|
|
5
|
+
export declare function maybeUploadSourceMapAsync(ctx: BuildContext<BuildJob>): Promise<void>;
|
|
@@ -0,0 +1,105 @@
|
|
|
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.isSourceMapUploadEnabled = isSourceMapUploadEnabled;
|
|
7
|
+
exports.resolveIosSourceMapPathAsync = resolveIosSourceMapPathAsync;
|
|
8
|
+
exports.maybeUploadSourceMapAsync = maybeUploadSourceMapAsync;
|
|
9
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
10
|
+
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
11
|
+
const fs_extra_1 = __importDefault(require("fs-extra"));
|
|
12
|
+
const path_1 = __importDefault(require("path"));
|
|
13
|
+
const ANDROID_SOURCE_MAP_PATTERN = 'android/**/build/generated/sourcemaps/react/**/*.map';
|
|
14
|
+
const SOURCE_MAP_UPLOAD_DIRECTORY = 'observe-source-maps';
|
|
15
|
+
function isSourceMapUploadEnabled(ctx) {
|
|
16
|
+
return !ctx.isLocal && ctx.job.experimental?.uploadSourceMaps === true;
|
|
17
|
+
}
|
|
18
|
+
async function resolveIosSourceMapPathAsync(ctx) {
|
|
19
|
+
const configuredPath = ctx.env.SOURCEMAP_FILE;
|
|
20
|
+
const sourceMapPath = configuredPath
|
|
21
|
+
? path_1.default.resolve(ctx.getReactNativeProjectDirectory(), 'ios', configuredPath)
|
|
22
|
+
: path_1.default.join(ctx.workingdir, SOURCE_MAP_UPLOAD_DIRECTORY, 'main.jsbundle.map');
|
|
23
|
+
await fs_extra_1.default.ensureDir(path_1.default.dirname(sourceMapPath));
|
|
24
|
+
return sourceMapPath;
|
|
25
|
+
}
|
|
26
|
+
async function maybeUploadSourceMapAsync(ctx) {
|
|
27
|
+
if (!isSourceMapUploadEnabled(ctx)) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try {
|
|
31
|
+
const sourceMapPath = await resolveSourceMapPathAsync(ctx);
|
|
32
|
+
const strippedSourceMapPath = await stripSourcesContentAsync(ctx, sourceMapPath);
|
|
33
|
+
ctx.logger.info(`Uploading source map: ${sourceMapPath}`);
|
|
34
|
+
await ctx.uploadArtifact({
|
|
35
|
+
artifact: {
|
|
36
|
+
type: eas_build_job_1.ManagedArtifactType.SOURCE_MAP,
|
|
37
|
+
paths: [strippedSourceMapPath],
|
|
38
|
+
},
|
|
39
|
+
logger: ctx.logger,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
ctx.logger.warn({ err }, 'Failed to upload source map.');
|
|
44
|
+
ctx.markBuildPhaseHasWarnings();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function resolveSourceMapPathAsync(ctx) {
|
|
48
|
+
if (ctx.job.platform === eas_build_job_1.Platform.IOS) {
|
|
49
|
+
const sourceMapPath = await resolveIosSourceMapPathAsync(ctx);
|
|
50
|
+
if (!(await fs_extra_1.default.pathExists(sourceMapPath))) {
|
|
51
|
+
throw new eas_build_job_1.SystemError(`The iOS source map was not generated at ${sourceMapPath}.`);
|
|
52
|
+
}
|
|
53
|
+
return sourceMapPath;
|
|
54
|
+
}
|
|
55
|
+
if (ctx.job.platform === eas_build_job_1.Platform.ANDROID) {
|
|
56
|
+
const projectDir = ctx.getReactNativeProjectDirectory();
|
|
57
|
+
const sourceMapPaths = (await (0, fast_glob_1.default)(ANDROID_SOURCE_MAP_PATTERN, {
|
|
58
|
+
absolute: true,
|
|
59
|
+
cwd: projectDir,
|
|
60
|
+
onlyFiles: true,
|
|
61
|
+
})).filter(sourceMapPath => !isIntermediateAndroidSourceMap(sourceMapPath));
|
|
62
|
+
if (sourceMapPaths.length === 0) {
|
|
63
|
+
throw new eas_build_job_1.SystemError('The Android build did not generate a final composed source map.');
|
|
64
|
+
}
|
|
65
|
+
if (sourceMapPaths.length > 1) {
|
|
66
|
+
throw new eas_build_job_1.SystemError(`Found multiple final Android source maps: ${sourceMapPaths.join(', ')}. ` +
|
|
67
|
+
'Refusing to upload a source map that may not match the application archive.');
|
|
68
|
+
}
|
|
69
|
+
return sourceMapPaths[0];
|
|
70
|
+
}
|
|
71
|
+
throw new eas_build_job_1.SystemError('Source-map upload is not supported for this build platform.');
|
|
72
|
+
}
|
|
73
|
+
function isIntermediateAndroidSourceMap(sourceMapPath) {
|
|
74
|
+
return sourceMapPath.endsWith('.packager.map') || sourceMapPath.endsWith('.compiler.map');
|
|
75
|
+
}
|
|
76
|
+
async function stripSourcesContentAsync(ctx, sourceMapPath) {
|
|
77
|
+
const sourceMap = JSON.parse(await fs_extra_1.default.readFile(sourceMapPath, 'utf8'));
|
|
78
|
+
if (!isRecord(sourceMap) || sourceMap.version !== 3) {
|
|
79
|
+
throw new eas_build_job_1.SystemError(`Invalid source map at ${sourceMapPath}.`);
|
|
80
|
+
}
|
|
81
|
+
removeSourcesContent(sourceMap);
|
|
82
|
+
const uploadDirectory = path_1.default.join(ctx.workingdir, SOURCE_MAP_UPLOAD_DIRECTORY);
|
|
83
|
+
const uploadPath = path_1.default.join(uploadDirectory, `${ctx.job.platform}.map`);
|
|
84
|
+
await fs_extra_1.default.ensureDir(uploadDirectory);
|
|
85
|
+
await fs_extra_1.default.writeFile(uploadPath, JSON.stringify(sourceMap), 'utf8');
|
|
86
|
+
return uploadPath;
|
|
87
|
+
}
|
|
88
|
+
function removeSourcesContent(value) {
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
for (const child of value) {
|
|
91
|
+
removeSourcesContent(child);
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (!isRecord(value)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
delete value.sourcesContent;
|
|
99
|
+
for (const child of Object.values(value)) {
|
|
100
|
+
removeSourcesContent(child);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function isRecord(value) {
|
|
104
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "22.0.0",
|
|
4
4
|
"bugs": "https://github.com/expo/eas-cli/issues",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Expo <support@expo.io>",
|
|
@@ -38,17 +38,17 @@
|
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@expo/config": "55.0.10",
|
|
40
40
|
"@expo/config-plugins": "55.0.7",
|
|
41
|
-
"@expo/downloader": "
|
|
42
|
-
"@expo/eas-build-job": "
|
|
41
|
+
"@expo/downloader": "22.0.0",
|
|
42
|
+
"@expo/eas-build-job": "22.0.0",
|
|
43
43
|
"@expo/env": "^0.4.0",
|
|
44
|
-
"@expo/logger": "
|
|
44
|
+
"@expo/logger": "22.0.0",
|
|
45
45
|
"@expo/package-manager": "1.9.10",
|
|
46
46
|
"@expo/plist": "^0.3.5",
|
|
47
47
|
"@expo/results": "^1.0.0",
|
|
48
48
|
"@expo/spawn-async": "1.7.2",
|
|
49
|
-
"@expo/steps": "
|
|
50
|
-
"@expo/template-file": "
|
|
51
|
-
"@expo/turtle-spawn": "
|
|
49
|
+
"@expo/steps": "22.0.0",
|
|
50
|
+
"@expo/template-file": "22.0.0",
|
|
51
|
+
"@expo/turtle-spawn": "22.0.0",
|
|
52
52
|
"@expo/xcpretty": "^4.3.1",
|
|
53
53
|
"@ngrok/ngrok": "1.7.0",
|
|
54
54
|
"@sentry/node": "7.77.0",
|
|
@@ -100,5 +100,5 @@
|
|
|
100
100
|
"typescript": "^5.5.4",
|
|
101
101
|
"uuid": "^9.0.1"
|
|
102
102
|
},
|
|
103
|
-
"gitHead": "
|
|
103
|
+
"gitHead": "b92a9cc5a718f133966e27b9fedcedb569f9cd17"
|
|
104
104
|
}
|