@expo/build-tools 22.5.0 → 23.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/ios.js +12 -0
- package/dist/common/installDependencies.js +5 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +8 -1
- package/dist/steps/functionGroups/build.js +20 -0
- package/dist/steps/functions/restoreBuildCache.d.ts +8 -0
- package/dist/steps/functions/restoreBuildCache.js +52 -0
- package/dist/steps/functions/saveBuildCache.d.ts +8 -0
- package/dist/steps/functions/saveBuildCache.js +52 -0
- package/dist/steps/functions/saveCache.d.ts +2 -1
- package/dist/steps/functions/saveCache.js +3 -1
- package/dist/steps/utils/remoteDeviceRunSession.js +2 -2
- package/dist/utils/cocoapodsCache.d.ts +19 -0
- package/dist/utils/cocoapodsCache.js +111 -0
- package/dist/utils/easCli.js +4 -3
- package/dist/utils/formatDuration.d.ts +1 -0
- package/dist/utils/formatDuration.js +19 -0
- package/dist/utils/processes.d.ts +7 -0
- package/dist/utils/processes.js +20 -0
- package/dist/utils/turtleSshSession.d.ts +28 -0
- package/dist/utils/turtleSshSession.js +182 -0
- package/dist/utils/upterm.d.ts +38 -0
- package/dist/utils/upterm.js +257 -0
- package/package.json +8 -8
package/dist/builders/ios.js
CHANGED
|
@@ -101,6 +101,12 @@ async function buildInnerAsync(ctx, jobHooksRef) {
|
|
|
101
101
|
env: ctx.env,
|
|
102
102
|
secrets: ctx.job.secrets,
|
|
103
103
|
});
|
|
104
|
+
await (0, restoreBuildCache_1.restoreCocoapodsCacheAsync)({
|
|
105
|
+
logger: ctx.logger,
|
|
106
|
+
workingDirectory,
|
|
107
|
+
env: ctx.env,
|
|
108
|
+
secrets: ctx.job.secrets,
|
|
109
|
+
});
|
|
104
110
|
});
|
|
105
111
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.INSTALL_PODS, async () => {
|
|
106
112
|
await runInstallPodsAsync(ctx);
|
|
@@ -229,6 +235,12 @@ async function buildInnerAsync(ctx, jobHooksRef) {
|
|
|
229
235
|
env: ctx.env,
|
|
230
236
|
secrets: ctx.job.secrets,
|
|
231
237
|
});
|
|
238
|
+
await (0, saveBuildCache_1.saveCocoapodsCacheAsync)({
|
|
239
|
+
logger: ctx.logger,
|
|
240
|
+
workingDirectory,
|
|
241
|
+
env: ctx.env,
|
|
242
|
+
secrets: ctx.job.secrets,
|
|
243
|
+
});
|
|
232
244
|
});
|
|
233
245
|
await ctx.runBuildPhase(eas_build_job_1.BuildPhase.CACHE_STATS, async () => {
|
|
234
246
|
await (0, restoreBuildCache_1.cacheStatsAsync)({
|
|
@@ -49,7 +49,11 @@ async function installDependenciesAsync({ packageManager, env, logger, infoCallb
|
|
|
49
49
|
break;
|
|
50
50
|
}
|
|
51
51
|
case packageManager_1.PackageManager.BUN:
|
|
52
|
-
args = [
|
|
52
|
+
args = [
|
|
53
|
+
'install',
|
|
54
|
+
...(env['EAS_BUN_FILTER_WORKSPACE'] ? ['--filter', env['EAS_BUN_FILTER_WORKSPACE']] : []),
|
|
55
|
+
...(useFrozenLockfile ? ['--frozen-lockfile'] : []),
|
|
56
|
+
];
|
|
53
57
|
break;
|
|
54
58
|
default:
|
|
55
59
|
throw new Error(`Unsupported package manager: ${packageManager}`);
|
package/dist/index.d.ts
CHANGED
|
@@ -4,8 +4,12 @@ import RemoteLoggerStream from './logging/RemoteLoggerStream';
|
|
|
4
4
|
export { Builders, HttpLogStream, RemoteLoggerStream };
|
|
5
5
|
export { uploadWithSignedUrl } from './storage/uploadWithSignedUrl';
|
|
6
6
|
export type { SignedUrl, UploadWithSignedUrlParams } from './storage/uploadWithSignedUrl';
|
|
7
|
+
export { downloadCacheAsync } from './steps/functions/restoreCache';
|
|
8
|
+
export { uploadCacheAsync } from './steps/functions/saveCache';
|
|
7
9
|
export { ArtifactToUpload, Artifacts, BuildContext, BuildContextOptions, CacheManager, LogBuffer, SkipNativeBuildError, } from './context';
|
|
8
10
|
export { PackageManager } from './utils/packageManager';
|
|
11
|
+
export * as TurtleSshSession from './utils/turtleSshSession';
|
|
12
|
+
export { formatSecondsForLog } from './utils/formatDuration';
|
|
9
13
|
export { findAndUploadXcodeBuildLogsAsync } from './ios/xcodeBuildLogs';
|
|
10
14
|
export { Hook, runHookIfPresent } from './utils/hooks';
|
|
11
15
|
export { parseGradleProfile, formatGradleProfileReport } from './android/gradleProfile';
|
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.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.HttpLogStream = exports.Builders = void 0;
|
|
42
|
+
exports.Sentry = exports.Datadog = exports.formatGradleProfileReport = exports.parseGradleProfile = exports.runHookIfPresent = exports.Hook = exports.findAndUploadXcodeBuildLogsAsync = exports.formatSecondsForLog = exports.TurtleSshSession = exports.PackageManager = exports.SkipNativeBuildError = exports.BuildContext = exports.uploadCacheAsync = exports.downloadCacheAsync = exports.uploadWithSignedUrl = exports.RemoteLoggerStream = exports.HttpLogStream = exports.Builders = void 0;
|
|
43
43
|
const Builders = __importStar(require("./builders"));
|
|
44
44
|
exports.Builders = Builders;
|
|
45
45
|
const HttpLogStream_1 = __importDefault(require("./logging/HttpLogStream"));
|
|
@@ -48,11 +48,18 @@ const RemoteLoggerStream_1 = __importDefault(require("./logging/RemoteLoggerStre
|
|
|
48
48
|
exports.RemoteLoggerStream = RemoteLoggerStream_1.default;
|
|
49
49
|
var uploadWithSignedUrl_1 = require("./storage/uploadWithSignedUrl");
|
|
50
50
|
Object.defineProperty(exports, "uploadWithSignedUrl", { enumerable: true, get: function () { return uploadWithSignedUrl_1.uploadWithSignedUrl; } });
|
|
51
|
+
var restoreCache_1 = require("./steps/functions/restoreCache");
|
|
52
|
+
Object.defineProperty(exports, "downloadCacheAsync", { enumerable: true, get: function () { return restoreCache_1.downloadCacheAsync; } });
|
|
53
|
+
var saveCache_1 = require("./steps/functions/saveCache");
|
|
54
|
+
Object.defineProperty(exports, "uploadCacheAsync", { enumerable: true, get: function () { return saveCache_1.uploadCacheAsync; } });
|
|
51
55
|
var context_1 = require("./context");
|
|
52
56
|
Object.defineProperty(exports, "BuildContext", { enumerable: true, get: function () { return context_1.BuildContext; } });
|
|
53
57
|
Object.defineProperty(exports, "SkipNativeBuildError", { enumerable: true, get: function () { return context_1.SkipNativeBuildError; } });
|
|
54
58
|
var packageManager_1 = require("./utils/packageManager");
|
|
55
59
|
Object.defineProperty(exports, "PackageManager", { enumerable: true, get: function () { return packageManager_1.PackageManager; } });
|
|
60
|
+
exports.TurtleSshSession = __importStar(require("./utils/turtleSshSession"));
|
|
61
|
+
var formatDuration_1 = require("./utils/formatDuration");
|
|
62
|
+
Object.defineProperty(exports, "formatSecondsForLog", { enumerable: true, get: function () { return formatDuration_1.formatSecondsForLog; } });
|
|
56
63
|
var xcodeBuildLogs_1 = require("./ios/xcodeBuildLogs");
|
|
57
64
|
Object.defineProperty(exports, "findAndUploadXcodeBuildLogsAsync", { enumerable: true, get: function () { return xcodeBuildLogs_1.findAndUploadXcodeBuildLogsAsync; } });
|
|
58
65
|
var hooks_1 = require("./utils/hooks");
|
|
@@ -80,6 +80,7 @@ function createEasBuildBuildFunctionGroup(buildToolsContext) {
|
|
|
80
80
|
});
|
|
81
81
|
}
|
|
82
82
|
function createStepsForIosSimulatorBuild({ globalCtx, buildToolsContext, workingDirectory, }) {
|
|
83
|
+
const evictUsedBefore = new Date();
|
|
83
84
|
const calculateEASUpdateRuntimeVersion = (0, calculateEASUpdateRuntimeVersion_1.calculateEASUpdateRuntimeVersionFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
84
85
|
id: 'calculate_eas_update_runtime_version',
|
|
85
86
|
workingDirectory,
|
|
@@ -87,6 +88,13 @@ function createStepsForIosSimulatorBuild({ globalCtx, buildToolsContext, working
|
|
|
87
88
|
const installPods = (0, installPods_1.createInstallPodsBuildFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
88
89
|
workingDirectory: workingDirectory ? path_1.default.join(workingDirectory, './ios') : './ios',
|
|
89
90
|
});
|
|
91
|
+
const restoreCache = (0, restoreBuildCache_1.createRestoreBuildCacheFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
92
|
+
workingDirectory,
|
|
93
|
+
callInputs: {
|
|
94
|
+
platform: eas_build_job_1.Platform.IOS,
|
|
95
|
+
simulator: true,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
90
98
|
const configureEASUpdate = (0, configureEASUpdateIfInstalled_1.configureEASUpdateIfInstalledFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
91
99
|
workingDirectory,
|
|
92
100
|
callInputs: {
|
|
@@ -101,6 +109,13 @@ function createStepsForIosSimulatorBuild({ globalCtx, buildToolsContext, working
|
|
|
101
109
|
resolved_eas_update_runtime_version: '${ steps.calculate_eas_update_runtime_version.resolved_eas_update_runtime_version }',
|
|
102
110
|
},
|
|
103
111
|
});
|
|
112
|
+
const saveCache = (0, saveBuildCache_1.createSaveBuildCacheFunction)(evictUsedBefore).createBuildStepFromFunctionCall(globalCtx, {
|
|
113
|
+
workingDirectory,
|
|
114
|
+
callInputs: {
|
|
115
|
+
platform: eas_build_job_1.Platform.IOS,
|
|
116
|
+
simulator: true,
|
|
117
|
+
},
|
|
118
|
+
});
|
|
104
119
|
return [
|
|
105
120
|
(0, checkout_1.createCheckoutBuildFunction)().createBuildStepFromFunctionCall(globalCtx),
|
|
106
121
|
(0, useNpmToken_1.createSetUpNpmrcBuildFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
@@ -113,6 +128,7 @@ function createStepsForIosSimulatorBuild({ globalCtx, buildToolsContext, working
|
|
|
113
128
|
(0, prebuild_1.createPrebuildBuildFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
114
129
|
workingDirectory,
|
|
115
130
|
}),
|
|
131
|
+
restoreCache,
|
|
116
132
|
calculateEASUpdateRuntimeVersion,
|
|
117
133
|
installPods,
|
|
118
134
|
configureEASUpdate,
|
|
@@ -131,6 +147,10 @@ function createStepsForIosSimulatorBuild({ globalCtx, buildToolsContext, working
|
|
|
131
147
|
}),
|
|
132
148
|
runFastlane,
|
|
133
149
|
(0, findAndUploadBuildArtifacts_1.createFindAndUploadBuildArtifactsBuildFunction)(buildToolsContext).createBuildStepFromFunctionCall(globalCtx, { workingDirectory }),
|
|
150
|
+
saveCache,
|
|
151
|
+
(0, restoreBuildCache_1.createCacheStatsBuildFunction)().createBuildStepFromFunctionCall(globalCtx, {
|
|
152
|
+
workingDirectory,
|
|
153
|
+
}),
|
|
134
154
|
];
|
|
135
155
|
}
|
|
136
156
|
function createStepsForIosBuildWithCredentials({ globalCtx, buildToolsContext, workingDirectory, }) {
|
|
@@ -12,6 +12,14 @@ export declare function restoreCcacheAsync({ logger, workingDirectory, target, e
|
|
|
12
12
|
robotAccessToken?: string;
|
|
13
13
|
};
|
|
14
14
|
}): Promise<void>;
|
|
15
|
+
export declare function restoreCocoapodsCacheAsync({ logger, workingDirectory, env, secrets, }: {
|
|
16
|
+
logger: bunyan;
|
|
17
|
+
workingDirectory: string;
|
|
18
|
+
env: Record<string, string | undefined>;
|
|
19
|
+
secrets?: {
|
|
20
|
+
robotAccessToken?: string;
|
|
21
|
+
};
|
|
22
|
+
}): Promise<void>;
|
|
15
23
|
export declare function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets, }: {
|
|
16
24
|
logger: bunyan;
|
|
17
25
|
workingDirectory: string;
|
|
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.createRestoreBuildCacheFunction = createRestoreBuildCacheFunction;
|
|
7
7
|
exports.createCacheStatsBuildFunction = createCacheStatsBuildFunction;
|
|
8
8
|
exports.restoreCcacheAsync = restoreCcacheAsync;
|
|
9
|
+
exports.restoreCocoapodsCacheAsync = restoreCocoapodsCacheAsync;
|
|
9
10
|
exports.restoreGradleCacheAsync = restoreGradleCacheAsync;
|
|
10
11
|
exports.cacheStatsAsync = cacheStatsAsync;
|
|
11
12
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
@@ -18,6 +19,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
18
19
|
const ccacheStats_1 = require("./ccacheStats");
|
|
19
20
|
const restoreCache_1 = require("./restoreCache");
|
|
20
21
|
const cacheKey_1 = require("../../utils/cacheKey");
|
|
22
|
+
const cocoapodsCache_1 = require("../../utils/cocoapodsCache");
|
|
21
23
|
const datadog_1 = require("../../datadog");
|
|
22
24
|
const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
|
|
23
25
|
const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
@@ -70,6 +72,14 @@ function createRestoreBuildCacheFunction() {
|
|
|
70
72
|
secrets: stepCtx.global.staticContext.job.secrets,
|
|
71
73
|
});
|
|
72
74
|
}
|
|
75
|
+
else {
|
|
76
|
+
await restoreCocoapodsCacheAsync({
|
|
77
|
+
logger,
|
|
78
|
+
workingDirectory,
|
|
79
|
+
env,
|
|
80
|
+
secrets: stepCtx.global.staticContext.job.secrets,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
73
83
|
},
|
|
74
84
|
});
|
|
75
85
|
}
|
|
@@ -167,6 +177,48 @@ async function restoreCcacheAsync({ logger, workingDirectory, target, env, secre
|
|
|
167
177
|
}
|
|
168
178
|
}
|
|
169
179
|
}
|
|
180
|
+
async function restoreCocoapodsCacheAsync({ logger, workingDirectory, env, secrets, }) {
|
|
181
|
+
if (env.EAS_PODS_CACHE !== '1') {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
try {
|
|
185
|
+
const { stdout } = await (0, steps_1.spawnAsync)('pod', ['--version'], {
|
|
186
|
+
env,
|
|
187
|
+
stdio: 'pipe',
|
|
188
|
+
});
|
|
189
|
+
const { key, keyPrefix } = await (0, cocoapodsCache_1.resolveCocoapodsCacheKeyAsync)(workingDirectory, stdout);
|
|
190
|
+
logger.info(`Restoring CocoaPods cache key: ${key}`);
|
|
191
|
+
const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
|
192
|
+
const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
|
|
193
|
+
const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
|
194
|
+
const { podsDirectory } = (0, cocoapodsCache_1.getCocoapodsCachePaths)(workingDirectory);
|
|
195
|
+
const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
|
|
196
|
+
logger,
|
|
197
|
+
jobId,
|
|
198
|
+
expoApiServerURL,
|
|
199
|
+
robotAccessToken,
|
|
200
|
+
paths: [podsDirectory],
|
|
201
|
+
key,
|
|
202
|
+
keyPrefixes: [keyPrefix],
|
|
203
|
+
platform: eas_build_job_1.Platform.IOS,
|
|
204
|
+
});
|
|
205
|
+
await (0, cocoapodsCache_1.restoreCocoapodsCacheArchiveAsync)({ archivePath, workingDirectory });
|
|
206
|
+
const hitType = matchedKey === key ? 'direct_hit' : 'prefix_match';
|
|
207
|
+
logger.info(`CocoaPods cache restored to ${podsDirectory} (${hitType === 'direct_hit' ? 'direct hit' : 'prefix match'})`);
|
|
208
|
+
datadog_1.Datadog.log(`CocoaPods cache restored (${hitType})`, {
|
|
209
|
+
event: 'cocoapods_cache_restored',
|
|
210
|
+
cache_hit_type: hitType,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
catch (err) {
|
|
214
|
+
if (err instanceof turtleFetch_1.TurtleFetchError && err.response?.status === 404) {
|
|
215
|
+
logger.info('No CocoaPods cache found for this key');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
logger.warn('Failed to restore CocoaPods cache: ', err);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
170
222
|
async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets, }) {
|
|
171
223
|
if (env.EAS_GRADLE_CACHE !== '1') {
|
|
172
224
|
return;
|
|
@@ -2,6 +2,14 @@ import { bunyan } from '@expo/logger';
|
|
|
2
2
|
import { BuildFunction } from '@expo/steps';
|
|
3
3
|
import { CcacheBuildTarget } from '../../utils/cacheKey';
|
|
4
4
|
export declare function createSaveBuildCacheFunction(evictUsedBefore: Date): BuildFunction;
|
|
5
|
+
export declare function saveCocoapodsCacheAsync({ logger, workingDirectory, env, secrets, }: {
|
|
6
|
+
logger: bunyan;
|
|
7
|
+
workingDirectory: string;
|
|
8
|
+
env: Record<string, string | undefined>;
|
|
9
|
+
secrets?: {
|
|
10
|
+
robotAccessToken?: string;
|
|
11
|
+
};
|
|
12
|
+
}): Promise<void>;
|
|
5
13
|
export declare function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }: {
|
|
6
14
|
logger: bunyan;
|
|
7
15
|
workingDirectory: string;
|
|
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.createSaveBuildCacheFunction = createSaveBuildCacheFunction;
|
|
7
|
+
exports.saveCocoapodsCacheAsync = saveCocoapodsCacheAsync;
|
|
7
8
|
exports.saveCcacheAsync = saveCcacheAsync;
|
|
8
9
|
exports.saveGradleCacheAsync = saveGradleCacheAsync;
|
|
9
10
|
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
@@ -16,6 +17,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
16
17
|
const saveCache_1 = require("./saveCache");
|
|
17
18
|
const artifacts_1 = require("../../utils/artifacts");
|
|
18
19
|
const cacheKey_1 = require("../../utils/cacheKey");
|
|
20
|
+
const cocoapodsCache_1 = require("../../utils/cocoapodsCache");
|
|
19
21
|
const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
|
|
20
22
|
function createSaveBuildCacheFunction(evictUsedBefore) {
|
|
21
23
|
return new steps_1.BuildFunction({
|
|
@@ -67,9 +69,59 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
|
|
|
67
69
|
secrets: stepCtx.global.staticContext.job.secrets,
|
|
68
70
|
});
|
|
69
71
|
}
|
|
72
|
+
else {
|
|
73
|
+
await saveCocoapodsCacheAsync({
|
|
74
|
+
logger,
|
|
75
|
+
workingDirectory,
|
|
76
|
+
env,
|
|
77
|
+
secrets: stepCtx.global.staticContext.job.secrets,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
70
80
|
},
|
|
71
81
|
});
|
|
72
82
|
}
|
|
83
|
+
async function saveCocoapodsCacheAsync({ logger, workingDirectory, env, secrets, }) {
|
|
84
|
+
if (env.EAS_PODS_CACHE !== '1') {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const { podsDirectory, podfileLockPath } = (0, cocoapodsCache_1.getCocoapodsCachePaths)(workingDirectory);
|
|
88
|
+
try {
|
|
89
|
+
await Promise.all([fs_1.default.promises.access(podsDirectory), fs_1.default.promises.access(podfileLockPath)]);
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
logger.warn('No CocoaPods installation found, skipping cache save');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const { stdout } = await (0, steps_1.spawnAsync)('pod', ['--version'], {
|
|
97
|
+
env,
|
|
98
|
+
stdio: 'pipe',
|
|
99
|
+
});
|
|
100
|
+
const { key } = await (0, cocoapodsCache_1.resolveCocoapodsCacheKeyAsync)(workingDirectory, stdout);
|
|
101
|
+
logger.info(`Saving CocoaPods cache key: ${key}`);
|
|
102
|
+
const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
|
|
103
|
+
const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
|
|
104
|
+
const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
|
|
105
|
+
logger.info('Compressing CocoaPods cache...');
|
|
106
|
+
const { archivePath } = await (0, cocoapodsCache_1.compressCocoapodsCacheAsync)({ workingDirectory });
|
|
107
|
+
const { size } = await fs_1.default.promises.stat(archivePath);
|
|
108
|
+
logger.info(`CocoaPods cache archive size: ${(0, artifacts_1.formatBytes)(size)}`);
|
|
109
|
+
await (0, saveCache_1.uploadCacheAsync)({
|
|
110
|
+
logger,
|
|
111
|
+
jobId,
|
|
112
|
+
expoApiServerURL,
|
|
113
|
+
robotAccessToken,
|
|
114
|
+
archivePath,
|
|
115
|
+
key,
|
|
116
|
+
paths: [podsDirectory],
|
|
117
|
+
size,
|
|
118
|
+
platform: eas_build_job_1.Platform.IOS,
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
logger.error({ err }, 'Failed to save CocoaPods cache');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
73
125
|
async function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }) {
|
|
74
126
|
const enabled = env.EAS_SAVE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_SAVE_CACHE !== '0');
|
|
75
127
|
if (!enabled) {
|
|
@@ -2,7 +2,7 @@ import { Platform } from '@expo/eas-build-job';
|
|
|
2
2
|
import { bunyan } from '@expo/logger';
|
|
3
3
|
import { BuildFunction } from '@expo/steps';
|
|
4
4
|
export declare function createSaveCacheFunction(): BuildFunction;
|
|
5
|
-
export declare function uploadCacheAsync({ logger, jobId, expoApiServerURL, robotAccessToken, paths, key, archivePath, size, platform, }: {
|
|
5
|
+
export declare function uploadCacheAsync({ logger, jobId, expoApiServerURL, robotAccessToken, paths, key, archivePath, size, platform, force, }: {
|
|
6
6
|
logger: bunyan;
|
|
7
7
|
jobId: string;
|
|
8
8
|
expoApiServerURL: string;
|
|
@@ -12,6 +12,7 @@ export declare function uploadCacheAsync({ logger, jobId, expoApiServerURL, robo
|
|
|
12
12
|
archivePath: string;
|
|
13
13
|
size: number;
|
|
14
14
|
platform: Platform | undefined;
|
|
15
|
+
force?: boolean;
|
|
15
16
|
}): Promise<void>;
|
|
16
17
|
export declare function uploadPublicCacheAsync({ logger, jobId, expoApiServerURL, robotAccessToken, paths, key, archivePath, size, }: {
|
|
17
18
|
logger: bunyan;
|
|
@@ -122,7 +122,7 @@ function createSaveCacheFunction() {
|
|
|
122
122
|
},
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
-
async function uploadCacheAsync({ logger, jobId, expoApiServerURL, robotAccessToken, paths, key, archivePath, size, platform, }) {
|
|
125
|
+
async function uploadCacheAsync({ logger, jobId, expoApiServerURL, robotAccessToken, paths, key, archivePath, size, platform, force = false, }) {
|
|
126
126
|
const routerURL = platform
|
|
127
127
|
? 'v2/turtle-builds/caches/upload-sessions'
|
|
128
128
|
: 'v2/turtle-caches/upload-sessions';
|
|
@@ -135,12 +135,14 @@ async function uploadCacheAsync({ logger, jobId, expoApiServerURL, robotAccessTo
|
|
|
135
135
|
key,
|
|
136
136
|
version: (0, cache_1.getCacheVersion)(paths),
|
|
137
137
|
size,
|
|
138
|
+
force,
|
|
138
139
|
})
|
|
139
140
|
: JSON.stringify({
|
|
140
141
|
jobRunId: jobId,
|
|
141
142
|
key,
|
|
142
143
|
version: (0, cache_1.getCacheVersion)(paths),
|
|
143
144
|
size,
|
|
145
|
+
force,
|
|
144
146
|
}),
|
|
145
147
|
headers: {
|
|
146
148
|
Authorization: `Bearer ${robotAccessToken}`,
|
|
@@ -71,9 +71,9 @@ const turtleFetch_1 = require("../../utils/turtleFetch");
|
|
|
71
71
|
const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
|
|
72
72
|
const SERVE_SIM_PACKAGE_NAME = '@expo/serve-sim';
|
|
73
73
|
const SERVE_SIM_HOST = '127.0.0.1';
|
|
74
|
-
const SERVE_SIM_MAX_DIMENSION = '
|
|
74
|
+
const SERVE_SIM_MAX_DIMENSION = '960';
|
|
75
75
|
const SERVE_SIM_MJPEG_QUALITY = '0.55';
|
|
76
|
-
const SERVE_SIM_VIDEO_BITRATE = '
|
|
76
|
+
const SERVE_SIM_VIDEO_BITRATE = '6000000';
|
|
77
77
|
const SERVE_SIM_VIDEO_FPS = '60';
|
|
78
78
|
const START_DEVICE_RUN_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
79
79
|
mutation StartDeviceRunSession($deviceRunSessionId: ID!, $remoteConfig: JSONObject!) {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const COCOAPODS_CACHE_KEY_PREFIX = "ios-pods-";
|
|
2
|
+
export declare function getCocoapodsCachePaths(workingDirectory: string): {
|
|
3
|
+
iosDirectory: string;
|
|
4
|
+
podsDirectory: string;
|
|
5
|
+
podfileLockPath: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function resolveCocoapodsCacheKeyAsync(workingDirectory: string, cocoapodsVersion: string): Promise<{
|
|
8
|
+
key: string;
|
|
9
|
+
keyPrefix: string;
|
|
10
|
+
}>;
|
|
11
|
+
export declare function compressCocoapodsCacheAsync({ workingDirectory, }: {
|
|
12
|
+
workingDirectory: string;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
archivePath: string;
|
|
15
|
+
}>;
|
|
16
|
+
export declare function restoreCocoapodsCacheArchiveAsync({ archivePath, workingDirectory, }: {
|
|
17
|
+
archivePath: string;
|
|
18
|
+
workingDirectory: string;
|
|
19
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.COCOAPODS_CACHE_KEY_PREFIX = void 0;
|
|
40
|
+
exports.getCocoapodsCachePaths = getCocoapodsCachePaths;
|
|
41
|
+
exports.resolveCocoapodsCacheKeyAsync = resolveCocoapodsCacheKeyAsync;
|
|
42
|
+
exports.compressCocoapodsCacheAsync = compressCocoapodsCacheAsync;
|
|
43
|
+
exports.restoreCocoapodsCacheArchiveAsync = restoreCocoapodsCacheArchiveAsync;
|
|
44
|
+
const steps_1 = require("@expo/steps");
|
|
45
|
+
const fs_1 = __importDefault(require("fs"));
|
|
46
|
+
const os_1 = __importDefault(require("os"));
|
|
47
|
+
const path_1 = __importDefault(require("path"));
|
|
48
|
+
const tar = __importStar(require("tar"));
|
|
49
|
+
exports.COCOAPODS_CACHE_KEY_PREFIX = 'ios-pods-';
|
|
50
|
+
const PODS_DIRECTORY_NAME = 'Pods';
|
|
51
|
+
const PODFILE_LOCK_NAME = 'Podfile.lock';
|
|
52
|
+
function getCocoapodsCachePaths(workingDirectory) {
|
|
53
|
+
const iosDirectory = path_1.default.join(workingDirectory, 'ios');
|
|
54
|
+
return {
|
|
55
|
+
iosDirectory,
|
|
56
|
+
podsDirectory: path_1.default.join(iosDirectory, PODS_DIRECTORY_NAME),
|
|
57
|
+
podfileLockPath: path_1.default.join(iosDirectory, PODFILE_LOCK_NAME),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function resolveCocoapodsCacheKeyAsync(workingDirectory, cocoapodsVersion) {
|
|
61
|
+
const normalizedVersion = cocoapodsVersion.trim();
|
|
62
|
+
if (!normalizedVersion) {
|
|
63
|
+
throw new Error('Failed to determine CocoaPods version');
|
|
64
|
+
}
|
|
65
|
+
const keyPrefix = `${exports.COCOAPODS_CACHE_KEY_PREFIX}${normalizedVersion}-`;
|
|
66
|
+
const { podfileLockPath } = getCocoapodsCachePaths(workingDirectory);
|
|
67
|
+
try {
|
|
68
|
+
await fs_1.default.promises.access(podfileLockPath);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return { key: keyPrefix, keyPrefix };
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
key: `${keyPrefix}${(0, steps_1.hashFiles)([podfileLockPath])}`,
|
|
75
|
+
keyPrefix,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async function compressCocoapodsCacheAsync({ workingDirectory, }) {
|
|
79
|
+
const { iosDirectory, podsDirectory } = getCocoapodsCachePaths(workingDirectory);
|
|
80
|
+
await fs_1.default.promises.access(podsDirectory);
|
|
81
|
+
const archiveDestinationDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(os_1.default.tmpdir(), 'save-cocoapods-cache-'));
|
|
82
|
+
const archivePath = path_1.default.join(archiveDestinationDirectory, 'cache.tar.gz');
|
|
83
|
+
await tar.create({
|
|
84
|
+
file: archivePath,
|
|
85
|
+
cwd: iosDirectory,
|
|
86
|
+
gzip: true,
|
|
87
|
+
}, [PODS_DIRECTORY_NAME]);
|
|
88
|
+
return { archivePath };
|
|
89
|
+
}
|
|
90
|
+
async function restoreCocoapodsCacheArchiveAsync({ archivePath, workingDirectory, }) {
|
|
91
|
+
const { iosDirectory, podsDirectory } = getCocoapodsCachePaths(workingDirectory);
|
|
92
|
+
await fs_1.default.promises.mkdir(iosDirectory, { recursive: true });
|
|
93
|
+
const temporaryRestoreDirectory = await fs_1.default.promises.mkdtemp(path_1.default.join(iosDirectory, '.eas-pods-cache-'));
|
|
94
|
+
try {
|
|
95
|
+
await tar.extract({
|
|
96
|
+
file: archivePath,
|
|
97
|
+
cwd: temporaryRestoreDirectory,
|
|
98
|
+
filter: entryPath => entryPath === PODS_DIRECTORY_NAME || entryPath.startsWith(`${PODS_DIRECTORY_NAME}/`),
|
|
99
|
+
});
|
|
100
|
+
const restoredPodsDirectory = path_1.default.join(temporaryRestoreDirectory, PODS_DIRECTORY_NAME);
|
|
101
|
+
const restoredPodsStat = await fs_1.default.promises.stat(restoredPodsDirectory);
|
|
102
|
+
if (!restoredPodsStat.isDirectory()) {
|
|
103
|
+
throw new Error('CocoaPods cache archive does not contain a Pods directory');
|
|
104
|
+
}
|
|
105
|
+
await fs_1.default.promises.rm(podsDirectory, { recursive: true, force: true });
|
|
106
|
+
await fs_1.default.promises.rename(restoredPodsDirectory, podsDirectory);
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
await fs_1.default.promises.rm(temporaryRestoreDirectory, { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
package/dist/utils/easCli.js
CHANGED
|
@@ -11,6 +11,7 @@ const eas_build_job_1 = require("@expo/eas-build-job");
|
|
|
11
11
|
const packageManager_1 = require("./packageManager");
|
|
12
12
|
const sentry_1 = require("../sentry");
|
|
13
13
|
let cachedEasCliVersionsPromise;
|
|
14
|
+
const NPX_ENV = { NPM_CONFIG_MIN_RELEASE_AGE: '0' };
|
|
14
15
|
/**
|
|
15
16
|
* Resolves the `eas-cli` versions to install for staging/production. Prefers the
|
|
16
17
|
* versions committed to `cli-versions.json` (fetched from expo/eas-cli); on any
|
|
@@ -66,7 +67,7 @@ async function resolveEasCommandPrefixAndEnvAsync() {
|
|
|
66
67
|
return {
|
|
67
68
|
cmd: 'npx',
|
|
68
69
|
args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
|
|
69
|
-
extraEnv:
|
|
70
|
+
extraEnv: NPX_ENV,
|
|
70
71
|
};
|
|
71
72
|
}
|
|
72
73
|
else if (process.env.ENVIRONMENT === 'staging') {
|
|
@@ -74,7 +75,7 @@ async function resolveEasCommandPrefixAndEnvAsync() {
|
|
|
74
75
|
return {
|
|
75
76
|
cmd: 'npx',
|
|
76
77
|
args: [...npxArgsPrefix, `eas-cli@${versions.STAGING}`],
|
|
77
|
-
extraEnv: { EXPO_STAGING: '1' },
|
|
78
|
+
extraEnv: { ...NPX_ENV, EXPO_STAGING: '1' },
|
|
78
79
|
};
|
|
79
80
|
}
|
|
80
81
|
else {
|
|
@@ -82,7 +83,7 @@ async function resolveEasCommandPrefixAndEnvAsync() {
|
|
|
82
83
|
return {
|
|
83
84
|
cmd: 'npx',
|
|
84
85
|
args: [...npxArgsPrefix, `eas-cli@${versions.PRODUCTION}`],
|
|
85
|
-
extraEnv:
|
|
86
|
+
extraEnv: NPX_ENV,
|
|
86
87
|
};
|
|
87
88
|
}
|
|
88
89
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function formatSecondsForLog(totalSeconds: number): string;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatSecondsForLog = formatSecondsForLog;
|
|
4
|
+
function formatSecondsForLog(totalSeconds) {
|
|
5
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
6
|
+
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
|
7
|
+
const seconds = totalSeconds % 60;
|
|
8
|
+
const parts = [];
|
|
9
|
+
if (hours > 0) {
|
|
10
|
+
parts.push(hours === 1 ? '1 hour' : `${hours} hours`);
|
|
11
|
+
}
|
|
12
|
+
if (minutes > 0) {
|
|
13
|
+
parts.push(minutes === 1 ? '1 minute' : `${minutes} minutes`);
|
|
14
|
+
}
|
|
15
|
+
if (seconds > 0 || parts.length === 0) {
|
|
16
|
+
parts.push(seconds === 1 ? '1 second' : `${seconds} seconds`);
|
|
17
|
+
}
|
|
18
|
+
return parts.join(' ');
|
|
19
|
+
}
|
|
@@ -1,2 +1,9 @@
|
|
|
1
|
+
import { ChildProcess } from 'node:child_process';
|
|
2
|
+
export declare function isChildProcessAlive(child: ChildProcess): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
|
|
5
|
+
* children cannot survive after the parent is gone (e.g. across upterm redial).
|
|
6
|
+
*/
|
|
7
|
+
export declare function killProcessGroup(child: ChildProcess): void;
|
|
1
8
|
export declare function getParentAndDescendantProcessPidsAsync(ppid: number): Promise<number[]>;
|
|
2
9
|
export declare function isProcessDescendantOfAsync(pid: number, ancestorPid: number): Promise<boolean>;
|
package/dist/utils/processes.js
CHANGED
|
@@ -3,9 +3,29 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isChildProcessAlive = isChildProcessAlive;
|
|
7
|
+
exports.killProcessGroup = killProcessGroup;
|
|
6
8
|
exports.getParentAndDescendantProcessPidsAsync = getParentAndDescendantProcessPidsAsync;
|
|
7
9
|
exports.isProcessDescendantOfAsync = isProcessDescendantOfAsync;
|
|
8
10
|
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
11
|
+
function isChildProcessAlive(child) {
|
|
12
|
+
return child.exitCode === null && child.signalCode === null && !child.killed;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Kill a detached spawn's process group. Negated pid targets the group so bash/sleep
|
|
16
|
+
* children cannot survive after the parent is gone (e.g. across upterm redial).
|
|
17
|
+
*/
|
|
18
|
+
function killProcessGroup(child) {
|
|
19
|
+
if (child.pid == null) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
process.kill(-child.pid, 'SIGTERM');
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
child.kill();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
9
29
|
async function getChildrenPidsAsync(parentPids) {
|
|
10
30
|
try {
|
|
11
31
|
const result = await (0, turtle_spawn_1.default)('pgrep', ['-P', parentPids.join(',')], {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Job, SshSettings } from '@expo/eas-build-job';
|
|
2
|
+
import { bunyan } from '@expo/logger';
|
|
3
|
+
import { BuildContext } from '../context';
|
|
4
|
+
export type TurtleSshTarget = {
|
|
5
|
+
type: 'BUILD' | 'JOB_RUN';
|
|
6
|
+
id: string;
|
|
7
|
+
};
|
|
8
|
+
export declare function isSshEnabled(job: Pick<Job, 'ssh'>): boolean;
|
|
9
|
+
export declare function getSshIdleTimeoutSeconds(job: Pick<Job, 'ssh'>): number;
|
|
10
|
+
export declare function getSshRelayServerUrl(job: Pick<Job, 'ssh'>): string;
|
|
11
|
+
export type SshSessionHandle = {
|
|
12
|
+
getConnectedClientCountAsync: () => Promise<number>;
|
|
13
|
+
ensureConnectedAsync: () => Promise<void>;
|
|
14
|
+
stopAsync: () => Promise<void>;
|
|
15
|
+
};
|
|
16
|
+
export type StartedSshSession = {
|
|
17
|
+
handle: SshSessionHandle;
|
|
18
|
+
idleTimeoutSeconds: number;
|
|
19
|
+
};
|
|
20
|
+
export declare function startSshSessionAsync(ctx: BuildContext, { target, relayServerUrl, idleTimeoutSeconds: requestedIdleTimeoutSeconds, }: {
|
|
21
|
+
target: TurtleSshTarget;
|
|
22
|
+
} & SshSettings): Promise<StartedSshSession>;
|
|
23
|
+
export declare function superviseSshSessionAsync({ handle, idleTimeoutSeconds, hasJobFinished, logger, }: {
|
|
24
|
+
handle: SshSessionHandle;
|
|
25
|
+
idleTimeoutSeconds: number;
|
|
26
|
+
hasJobFinished: () => boolean;
|
|
27
|
+
logger: bunyan;
|
|
28
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isSshEnabled = isSshEnabled;
|
|
4
|
+
exports.getSshIdleTimeoutSeconds = getSshIdleTimeoutSeconds;
|
|
5
|
+
exports.getSshRelayServerUrl = getSshRelayServerUrl;
|
|
6
|
+
exports.startSshSessionAsync = startSshSessionAsync;
|
|
7
|
+
exports.superviseSshSessionAsync = superviseSshSessionAsync;
|
|
8
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
9
|
+
const gql_tada_1 = require("gql.tada");
|
|
10
|
+
const formatDuration_1 = require("./formatDuration");
|
|
11
|
+
const retry_1 = require("./retry");
|
|
12
|
+
const upterm_1 = require("./upterm");
|
|
13
|
+
const sentry_1 = require("../sentry");
|
|
14
|
+
const MAX_SSH_REDIALS = 10;
|
|
15
|
+
const REDIAL_BACKOFF_MS = 6_000;
|
|
16
|
+
const CLIENT_COUNT_POLL_INTERVAL_MS = 5_000;
|
|
17
|
+
const MAX_SSH_IDLE_TIMEOUT_SECONDS = 3600;
|
|
18
|
+
const DEFAULT_SSH_IDLE_TIMEOUT_SECONDS = 0;
|
|
19
|
+
const CREATE_OR_UPDATE_TURTLE_SSH_SESSION_MUTATION = (0, gql_tada_1.graphql)(`
|
|
20
|
+
mutation CreateOrUpdateTurtleSshSession(
|
|
21
|
+
$target: TurtleSshTargetInput!
|
|
22
|
+
$connectionConfig: TurtleSshConnectionConfigInput!
|
|
23
|
+
$sessionSettings: TurtleSshSessionSettingsInput!
|
|
24
|
+
) {
|
|
25
|
+
turtleSshSession {
|
|
26
|
+
createOrUpdateTurtleSshSession(
|
|
27
|
+
target: $target
|
|
28
|
+
connectionConfig: $connectionConfig
|
|
29
|
+
sessionSettings: $sessionSettings
|
|
30
|
+
) {
|
|
31
|
+
id
|
|
32
|
+
sessionSettings {
|
|
33
|
+
idleTimeoutSeconds
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
`);
|
|
39
|
+
function isSshEnabled(job) {
|
|
40
|
+
return job.ssh != null;
|
|
41
|
+
}
|
|
42
|
+
function getSshIdleTimeoutSeconds(job) {
|
|
43
|
+
const idleTimeoutSeconds = job.ssh?.idleTimeoutSeconds ?? DEFAULT_SSH_IDLE_TIMEOUT_SECONDS;
|
|
44
|
+
if (!Number.isInteger(idleTimeoutSeconds) ||
|
|
45
|
+
idleTimeoutSeconds < 0 ||
|
|
46
|
+
idleTimeoutSeconds > MAX_SSH_IDLE_TIMEOUT_SECONDS) {
|
|
47
|
+
throw new eas_build_job_1.SystemError(`SSH idle timeout must be an integer between 0 and ${MAX_SSH_IDLE_TIMEOUT_SECONDS} seconds, got ${idleTimeoutSeconds}.`, { trackingCode: 'SSH_IDLE_TIMEOUT_INVALID' });
|
|
48
|
+
}
|
|
49
|
+
return idleTimeoutSeconds;
|
|
50
|
+
}
|
|
51
|
+
function getSshRelayServerUrl(job) {
|
|
52
|
+
const relayServerUrl = job.ssh?.relayServerUrl;
|
|
53
|
+
if (!relayServerUrl) {
|
|
54
|
+
throw new eas_build_job_1.SystemError('SSH is enabled but no relay server URL was configured on the job.', {
|
|
55
|
+
trackingCode: 'SSH_RELAY_SERVER_URL_MISSING',
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return relayServerUrl;
|
|
59
|
+
}
|
|
60
|
+
async function createOrUpdateSessionAsync(ctx, { target, connectionConfig, idleTimeoutSeconds, }) {
|
|
61
|
+
const result = await ctx.graphqlClient
|
|
62
|
+
.mutation(CREATE_OR_UPDATE_TURTLE_SSH_SESSION_MUTATION, {
|
|
63
|
+
target,
|
|
64
|
+
connectionConfig: {
|
|
65
|
+
...connectionConfig,
|
|
66
|
+
type: 'UPTERM_V1',
|
|
67
|
+
},
|
|
68
|
+
sessionSettings: { idleTimeoutSeconds },
|
|
69
|
+
})
|
|
70
|
+
.toPromise();
|
|
71
|
+
if (result.error || !result.data) {
|
|
72
|
+
throw new eas_build_job_1.SystemError(`Failed to create or update the SSH session: ${result.error?.message ?? 'no data returned'}`, { cause: result.error });
|
|
73
|
+
}
|
|
74
|
+
const session = result.data.turtleSshSession.createOrUpdateTurtleSshSession;
|
|
75
|
+
return { idleTimeoutSeconds: session.sessionSettings.idleTimeoutSeconds };
|
|
76
|
+
}
|
|
77
|
+
async function startSshSessionAsync(ctx, { target, relayServerUrl, idleTimeoutSeconds: requestedIdleTimeoutSeconds, }) {
|
|
78
|
+
const logger = ctx.logger;
|
|
79
|
+
const host = await (0, upterm_1.startUptermHostAsync)(ctx, { relayServerUrl });
|
|
80
|
+
let idleTimeoutSeconds;
|
|
81
|
+
try {
|
|
82
|
+
({ idleTimeoutSeconds } = await createOrUpdateSessionAsync(ctx, {
|
|
83
|
+
target,
|
|
84
|
+
connectionConfig: { ...host.connectionConfig, reconnecting: false },
|
|
85
|
+
idleTimeoutSeconds: requestedIdleTimeoutSeconds,
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
catch (err) {
|
|
89
|
+
await host.stopAsync().catch(() => { });
|
|
90
|
+
throw err;
|
|
91
|
+
}
|
|
92
|
+
const ensureConnectedAsync = async () => {
|
|
93
|
+
if (host.isAlive()) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
for (let attempt = 1; attempt <= MAX_SSH_REDIALS; attempt++) {
|
|
97
|
+
try {
|
|
98
|
+
if (!host.isAlive()) {
|
|
99
|
+
logger.warn('The SSH relay connection dropped. Reconnecting...');
|
|
100
|
+
await createOrUpdateSessionAsync(ctx, {
|
|
101
|
+
target,
|
|
102
|
+
connectionConfig: { ...host.connectionConfig, reconnecting: true },
|
|
103
|
+
idleTimeoutSeconds: requestedIdleTimeoutSeconds,
|
|
104
|
+
}).catch(() => { });
|
|
105
|
+
await host.redialAsync();
|
|
106
|
+
}
|
|
107
|
+
await createOrUpdateSessionAsync(ctx, {
|
|
108
|
+
target,
|
|
109
|
+
connectionConfig: { ...host.connectionConfig, reconnecting: false },
|
|
110
|
+
idleTimeoutSeconds: requestedIdleTimeoutSeconds,
|
|
111
|
+
});
|
|
112
|
+
logger.info('The SSH relay connection was restored.');
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
logger.warn({ err }, `SSH reconnect attempt ${attempt} of ${MAX_SSH_REDIALS} failed.`);
|
|
117
|
+
if (attempt < MAX_SSH_REDIALS) {
|
|
118
|
+
await (0, retry_1.sleepAsync)(REDIAL_BACKOFF_MS);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
throw new eas_build_job_1.SystemError(`The SSH relay connection dropped and could not be restored after ${MAX_SSH_REDIALS} attempts.`);
|
|
123
|
+
};
|
|
124
|
+
return {
|
|
125
|
+
handle: {
|
|
126
|
+
getConnectedClientCountAsync: () => host.getConnectedClientCountAsync(),
|
|
127
|
+
ensureConnectedAsync,
|
|
128
|
+
stopAsync: () => host.stopAsync(),
|
|
129
|
+
},
|
|
130
|
+
idleTimeoutSeconds,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
async function superviseSshSessionAsync({ handle, idleTimeoutSeconds, hasJobFinished, logger, }) {
|
|
134
|
+
const idleTimeoutMs = idleTimeoutSeconds * 1_000;
|
|
135
|
+
let idleSince = null;
|
|
136
|
+
let previousClientCount = 0;
|
|
137
|
+
for (;;) {
|
|
138
|
+
try {
|
|
139
|
+
await handle.ensureConnectedAsync();
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
logger.warn({ err }, 'Could not restore the SSH relay connection. Closing the session.');
|
|
143
|
+
sentry_1.Sentry.capture('Could not restore the SSH relay connection', err instanceof Error ? err : undefined, {
|
|
144
|
+
tags: { trackingCode: 'SSH_RELAY_RECONNECT_FAILED' },
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
let connectedClientCount;
|
|
149
|
+
try {
|
|
150
|
+
connectedClientCount = await handle.getConnectedClientCountAsync();
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
logger.warn({ err }, 'Could not read the SSH client count. Closing the session.');
|
|
154
|
+
sentry_1.Sentry.capture('Could not read the SSH client count', err instanceof Error ? err : undefined, {
|
|
155
|
+
tags: { trackingCode: 'SSH_CLIENT_COUNT_UNREADABLE' },
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (connectedClientCount !== previousClientCount) {
|
|
160
|
+
logger.info(`SSH clients connected: ${connectedClientCount}`);
|
|
161
|
+
previousClientCount = connectedClientCount;
|
|
162
|
+
}
|
|
163
|
+
const jobHasFinished = hasJobFinished();
|
|
164
|
+
if (connectedClientCount > 0 || !jobHasFinished) {
|
|
165
|
+
idleSince = null;
|
|
166
|
+
}
|
|
167
|
+
else if (idleSince === null) {
|
|
168
|
+
idleSince = Date.now();
|
|
169
|
+
}
|
|
170
|
+
if (jobHasFinished && connectedClientCount === 0) {
|
|
171
|
+
if (idleTimeoutSeconds === 0) {
|
|
172
|
+
logger.info('The job finished and no SSH client is connected. Closing the session.');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (idleSince !== null && Date.now() - idleSince >= idleTimeoutMs) {
|
|
176
|
+
logger.info(`No SSH client connected for ${(0, formatDuration_1.formatSecondsForLog)(idleTimeoutSeconds)} after the job finished. Closing the session.`);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
await (0, retry_1.sleepAsync)(CLIENT_COUNT_POLL_INTERVAL_MS);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Env } from '@expo/eas-build-job';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BuildContext } from '../context';
|
|
4
|
+
export declare function resolveUptermGcsObjectName(platform?: NodeJS.Platform, arch?: string): string;
|
|
5
|
+
export type SshConnectionConfig = {
|
|
6
|
+
type: 'upterm-v1';
|
|
7
|
+
host: string;
|
|
8
|
+
secret: string;
|
|
9
|
+
};
|
|
10
|
+
export type UptermHost = {
|
|
11
|
+
connectionConfig: SshConnectionConfig;
|
|
12
|
+
getConnectedClientCountAsync: () => Promise<number>;
|
|
13
|
+
isAlive: () => boolean;
|
|
14
|
+
redialAsync: () => Promise<SshConnectionConfig>;
|
|
15
|
+
stopAsync: () => Promise<void>;
|
|
16
|
+
};
|
|
17
|
+
declare const UptermSessionJsonZ: z.ZodObject<{
|
|
18
|
+
sessionId: z.ZodString;
|
|
19
|
+
host: z.ZodString;
|
|
20
|
+
clientCount: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
}, z.core.$strip>;
|
|
22
|
+
type UptermSessionJson = z.infer<typeof UptermSessionJsonZ>;
|
|
23
|
+
export declare function connectionConfigFromUptermSession(parsed: Pick<UptermSessionJson, 'sessionId' | 'host'>): SshConnectionConfig | null;
|
|
24
|
+
/**
|
|
25
|
+
* Strip upterm session secrets out of text before it reaches a log or an error message. upterm
|
|
26
|
+
* prints the session id as the userinfo of the connect line it advertises
|
|
27
|
+
* (`upterm proxy ws(s)://<sessionId>@host`) and repeats it bare as the ssh destination on the
|
|
28
|
+
* same line. Control characters are dropped first so they cannot split a token mid-match. The
|
|
29
|
+
* loop lifts the id out of the proxy URL and removes every occurrence of it; the final replace
|
|
30
|
+
* blanks userinfo in any other URL as a catch-all.
|
|
31
|
+
*/
|
|
32
|
+
export declare function redactConnectionSecrets(text: string): string;
|
|
33
|
+
export declare function redactSpawnErrorForLog(err: unknown): unknown;
|
|
34
|
+
export declare function resolveUptermPathAsync(env: Env): Promise<string>;
|
|
35
|
+
export declare function startUptermHostAsync(ctx: BuildContext, { relayServerUrl }: {
|
|
36
|
+
relayServerUrl: string;
|
|
37
|
+
}): Promise<UptermHost>;
|
|
38
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
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.resolveUptermGcsObjectName = resolveUptermGcsObjectName;
|
|
7
|
+
exports.connectionConfigFromUptermSession = connectionConfigFromUptermSession;
|
|
8
|
+
exports.redactConnectionSecrets = redactConnectionSecrets;
|
|
9
|
+
exports.redactSpawnErrorForLog = redactSpawnErrorForLog;
|
|
10
|
+
exports.resolveUptermPathAsync = resolveUptermPathAsync;
|
|
11
|
+
exports.startUptermHostAsync = startUptermHostAsync;
|
|
12
|
+
const eas_build_job_1 = require("@expo/eas-build-job");
|
|
13
|
+
const downloader_1 = __importDefault(require("@expo/downloader"));
|
|
14
|
+
const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
|
|
15
|
+
const promises_1 = __importDefault(require("node:fs/promises"));
|
|
16
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
17
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
18
|
+
const zod_1 = require("zod");
|
|
19
|
+
const processes_1 = require("./processes");
|
|
20
|
+
const retry_1 = require("./retry");
|
|
21
|
+
const CONTROL_CHARACTERS = /[\u0000-\u0008\u000B-\u001F\u007F-\u009F]/g;
|
|
22
|
+
function resolveUptermGcsObjectName(platform = process.platform, arch = process.arch) {
|
|
23
|
+
if (platform === 'darwin' && arch === 'arm64') {
|
|
24
|
+
return 'upterm-darwin-arm64';
|
|
25
|
+
}
|
|
26
|
+
if (platform === 'linux' && arch === 'x64') {
|
|
27
|
+
return 'upterm-linux-amd64';
|
|
28
|
+
}
|
|
29
|
+
throw new eas_build_job_1.SystemError(`SSH upterm is only available on darwin/arm64 and linux/x64 (got ${platform}/${arch}).`);
|
|
30
|
+
}
|
|
31
|
+
const UPTERM_GCS_BASE_URL = 'https://storage.googleapis.com/turtle-v2/upterm';
|
|
32
|
+
const UPTERM_DOWNLOAD_TIMEOUT_MS = 60_000;
|
|
33
|
+
const UPTERM_KEEPALIVE_SLEEP_SECONDS = 6 * 60 * 60;
|
|
34
|
+
const CONNECTION_POLL_INTERVAL_MS = 500;
|
|
35
|
+
const CONNECTION_STARTUP_TIMEOUT_MS = 60_000;
|
|
36
|
+
const PROCESS_EXIT_TIMEOUT_MS = 5_000;
|
|
37
|
+
const CLIENT_COUNT_READ_ATTEMPTS = 4;
|
|
38
|
+
const CLIENT_COUNT_READ_RETRY_MS = 500;
|
|
39
|
+
const DEFAULT_SSH_PORT = '22';
|
|
40
|
+
const UptermSessionJsonZ = zod_1.z.object({
|
|
41
|
+
sessionId: zod_1.z.string().min(1),
|
|
42
|
+
host: zod_1.z.string().min(1),
|
|
43
|
+
clientCount: zod_1.z.number().optional(),
|
|
44
|
+
});
|
|
45
|
+
function connectionConfigFromUptermSession(parsed) {
|
|
46
|
+
let host = parsed.host;
|
|
47
|
+
if (host.includes('://')) {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(host);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
host = url.port && url.port !== DEFAULT_SSH_PORT ? `${url.hostname}:${url.port}` : url.hostname;
|
|
56
|
+
}
|
|
57
|
+
if (!host) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
return { type: 'upterm-v1', host, secret: parsed.sessionId };
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Strip upterm session secrets out of text before it reaches a log or an error message. upterm
|
|
64
|
+
* prints the session id as the userinfo of the connect line it advertises
|
|
65
|
+
* (`upterm proxy ws(s)://<sessionId>@host`) and repeats it bare as the ssh destination on the
|
|
66
|
+
* same line. Control characters are dropped first so they cannot split a token mid-match. The
|
|
67
|
+
* loop lifts the id out of the proxy URL and removes every occurrence of it; the final replace
|
|
68
|
+
* blanks userinfo in any other URL as a catch-all.
|
|
69
|
+
*/
|
|
70
|
+
function redactConnectionSecrets(text) {
|
|
71
|
+
let redacted = text.replace(CONTROL_CHARACTERS, '');
|
|
72
|
+
for (const [, token] of redacted.matchAll(/upterm proxy wss?:\/\/([^@\s]+)@/g)) {
|
|
73
|
+
redacted = redacted.split(token).join('<redacted>');
|
|
74
|
+
}
|
|
75
|
+
return redacted.replace(/([a-z][a-z0-9+.-]*:\/\/)[^@\s/]+@/gi, '$1<redacted>@');
|
|
76
|
+
}
|
|
77
|
+
function redactSpawnErrorForLog(err) {
|
|
78
|
+
if (!err || typeof err !== 'object') {
|
|
79
|
+
return err;
|
|
80
|
+
}
|
|
81
|
+
const spawnErr = err;
|
|
82
|
+
return {
|
|
83
|
+
...spawnErr,
|
|
84
|
+
...(typeof spawnErr.message === 'string'
|
|
85
|
+
? { message: redactConnectionSecrets(spawnErr.message) }
|
|
86
|
+
: {}),
|
|
87
|
+
...(typeof spawnErr.stdout === 'string'
|
|
88
|
+
? { stdout: redactConnectionSecrets(spawnErr.stdout) }
|
|
89
|
+
: {}),
|
|
90
|
+
...(typeof spawnErr.stderr === 'string'
|
|
91
|
+
? { stderr: redactConnectionSecrets(spawnErr.stderr) }
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function resolveUptermPathAsync(env) {
|
|
96
|
+
try {
|
|
97
|
+
await (0, turtle_spawn_1.default)('upterm', ['version'], { stdio: 'pipe', env });
|
|
98
|
+
return 'upterm';
|
|
99
|
+
}
|
|
100
|
+
catch { }
|
|
101
|
+
const objectName = resolveUptermGcsObjectName();
|
|
102
|
+
const downloadDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-upterm-'));
|
|
103
|
+
const uptermPath = node_path_1.default.join(downloadDir, objectName);
|
|
104
|
+
const url = `${UPTERM_GCS_BASE_URL}/${objectName}`;
|
|
105
|
+
try {
|
|
106
|
+
await (0, downloader_1.default)(url, uptermPath, { retry: 3, timeout: UPTERM_DOWNLOAD_TIMEOUT_MS });
|
|
107
|
+
await promises_1.default.chmod(uptermPath, 0o755);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
await promises_1.default.rm(downloadDir, { recursive: true, force: true }).catch(() => { });
|
|
111
|
+
throw new eas_build_job_1.SystemError(`The upterm SSH client was not on PATH and could not be downloaded from ${url}. ${err instanceof Error ? err.message : String(err)}`);
|
|
112
|
+
}
|
|
113
|
+
return uptermPath;
|
|
114
|
+
}
|
|
115
|
+
async function findAdminSocketPathAsync(uptermSocketDir) {
|
|
116
|
+
// Use this dial's own admin socket, not upterm's default, which can still point at a previous
|
|
117
|
+
// dial's session after a redial and break client-count reads.
|
|
118
|
+
const entries = await promises_1.default.readdir(uptermSocketDir).catch(() => []);
|
|
119
|
+
const socketName = entries.find(entry => entry.endsWith('.sock'));
|
|
120
|
+
return socketName ? node_path_1.default.join(uptermSocketDir, socketName) : null;
|
|
121
|
+
}
|
|
122
|
+
async function readCurrentSessionJsonAsync(uptermPath, adminSocketPath) {
|
|
123
|
+
try {
|
|
124
|
+
const result = await (0, turtle_spawn_1.default)(uptermPath, ['session', 'current', '--admin-socket', adminSocketPath, '--output', 'json'], { stdio: 'pipe' });
|
|
125
|
+
const parsed = UptermSessionJsonZ.safeParse(JSON.parse(result.stdout));
|
|
126
|
+
return parsed.success ? parsed.data : null;
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
async function waitForConnectionConfigAsync(uptermPath, uptermSocketDir, getHostOutput) {
|
|
133
|
+
const deadline = Date.now() + CONNECTION_STARTUP_TIMEOUT_MS;
|
|
134
|
+
for (;;) {
|
|
135
|
+
const adminSocketPath = await findAdminSocketPathAsync(uptermSocketDir);
|
|
136
|
+
if (adminSocketPath) {
|
|
137
|
+
const session = await readCurrentSessionJsonAsync(uptermPath, adminSocketPath);
|
|
138
|
+
if (session) {
|
|
139
|
+
const connectionConfig = connectionConfigFromUptermSession(session);
|
|
140
|
+
if (connectionConfig) {
|
|
141
|
+
return connectionConfig;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (Date.now() >= deadline) {
|
|
146
|
+
throw new eas_build_job_1.SystemError(`The upterm client did not register with the relay within ${CONNECTION_STARTUP_TIMEOUT_MS / 1_000}s. Output:\n${redactConnectionSecrets(getHostOutput())}`);
|
|
147
|
+
}
|
|
148
|
+
await (0, retry_1.sleepAsync)(CONNECTION_POLL_INTERVAL_MS);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
async function startUptermHostAsync(ctx, { relayServerUrl }) {
|
|
152
|
+
const uptermPath = await resolveUptermPathAsync(ctx.env);
|
|
153
|
+
const stateDir = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'eas-ssh-'));
|
|
154
|
+
const hostKeyPath = node_path_1.default.join(stateDir, 'id_host');
|
|
155
|
+
const forceCommandPath = node_path_1.default.join(stateDir, 'join.sh');
|
|
156
|
+
const uptermSocketDir = node_path_1.default.join(stateDir, 'upterm');
|
|
157
|
+
await (0, turtle_spawn_1.default)('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', hostKeyPath, '-q'], {
|
|
158
|
+
logger: ctx.logger,
|
|
159
|
+
});
|
|
160
|
+
await promises_1.default.writeFile(forceCommandPath, '#!/usr/bin/env bash\nexec bash -l\n', { mode: 0o755 });
|
|
161
|
+
let currentProcess = null;
|
|
162
|
+
const stopCurrentProcessAsync = async () => {
|
|
163
|
+
const previousProcess = currentProcess;
|
|
164
|
+
currentProcess = null;
|
|
165
|
+
if (!previousProcess) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
(0, processes_1.killProcessGroup)(previousProcess.child);
|
|
169
|
+
await Promise.race([
|
|
170
|
+
previousProcess.catch(() => { }),
|
|
171
|
+
(0, retry_1.sleepAsync)(PROCESS_EXIT_TIMEOUT_MS).then(() => {
|
|
172
|
+
ctx.logger.debug('The previous upterm host process did not exit in time.');
|
|
173
|
+
}),
|
|
174
|
+
]);
|
|
175
|
+
};
|
|
176
|
+
const getConnectedClientCountAsync = async () => {
|
|
177
|
+
for (let attempt = 1; attempt <= CLIENT_COUNT_READ_ATTEMPTS; attempt++) {
|
|
178
|
+
const adminSocketPath = await findAdminSocketPathAsync(uptermSocketDir);
|
|
179
|
+
const session = adminSocketPath
|
|
180
|
+
? await readCurrentSessionJsonAsync(uptermPath, adminSocketPath)
|
|
181
|
+
: null;
|
|
182
|
+
if (session && typeof session.clientCount === 'number') {
|
|
183
|
+
return session.clientCount;
|
|
184
|
+
}
|
|
185
|
+
if (attempt < CLIENT_COUNT_READ_ATTEMPTS) {
|
|
186
|
+
await (0, retry_1.sleepAsync)(CLIENT_COUNT_READ_RETRY_MS);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
throw new eas_build_job_1.SystemError('Could not read the SSH client count from the upterm admin socket.', {
|
|
190
|
+
trackingCode: 'SSH_CLIENT_COUNT_UNREADABLE',
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
const dialAsync = async () => {
|
|
194
|
+
await stopCurrentProcessAsync();
|
|
195
|
+
await promises_1.default.rm(uptermSocketDir, { recursive: true, force: true }).catch(err => {
|
|
196
|
+
ctx.logger.debug({ err }, 'Failed to clear the previous SSH socket directory.');
|
|
197
|
+
});
|
|
198
|
+
ctx.logger.debug('Connecting to the SSH relay.');
|
|
199
|
+
// --force-command is what each connecting SSH client runs (a login shell). The `sleep` after
|
|
200
|
+
// `--` is the host-side process that keeps `upterm host` up while nobody is connected.
|
|
201
|
+
const uptermProcess = (0, turtle_spawn_1.default)(uptermPath, [
|
|
202
|
+
'host',
|
|
203
|
+
'--server',
|
|
204
|
+
relayServerUrl,
|
|
205
|
+
'--accept',
|
|
206
|
+
'--skip-host-key-check',
|
|
207
|
+
'-i',
|
|
208
|
+
hostKeyPath,
|
|
209
|
+
'--force-command',
|
|
210
|
+
forceCommandPath,
|
|
211
|
+
'--',
|
|
212
|
+
'bash',
|
|
213
|
+
'-lc',
|
|
214
|
+
`sleep ${UPTERM_KEEPALIVE_SLEEP_SECONDS}`,
|
|
215
|
+
], {
|
|
216
|
+
// upterm puts its admin socket under XDG_RUNTIME_DIR; point it at our state dir so we can
|
|
217
|
+
// find it for `session current` and clean it up on stop/redial.
|
|
218
|
+
env: { ...ctx.env, XDG_RUNTIME_DIR: stateDir },
|
|
219
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
220
|
+
detached: true,
|
|
221
|
+
});
|
|
222
|
+
uptermProcess.catch(err => ctx.logger.debug({ err: redactSpawnErrorForLog(err) }, 'The upterm host process exited.'));
|
|
223
|
+
uptermProcess.child.unref();
|
|
224
|
+
currentProcess = uptermProcess;
|
|
225
|
+
let output = '';
|
|
226
|
+
const appendChunk = (chunk) => {
|
|
227
|
+
output += chunk.toString();
|
|
228
|
+
};
|
|
229
|
+
uptermProcess.child.stdout?.on('data', appendChunk);
|
|
230
|
+
uptermProcess.child.stderr?.on('data', appendChunk);
|
|
231
|
+
return await waitForConnectionConfigAsync(uptermPath, uptermSocketDir, () => output);
|
|
232
|
+
};
|
|
233
|
+
const stopAsync = async () => {
|
|
234
|
+
await stopCurrentProcessAsync();
|
|
235
|
+
await promises_1.default.rm(stateDir, { recursive: true, force: true });
|
|
236
|
+
};
|
|
237
|
+
let connectionConfig;
|
|
238
|
+
try {
|
|
239
|
+
connectionConfig = await dialAsync();
|
|
240
|
+
}
|
|
241
|
+
catch (err) {
|
|
242
|
+
await stopAsync();
|
|
243
|
+
throw err;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
get connectionConfig() {
|
|
247
|
+
return connectionConfig;
|
|
248
|
+
},
|
|
249
|
+
getConnectedClientCountAsync,
|
|
250
|
+
isAlive: () => currentProcess != null && (0, processes_1.isChildProcessAlive)(currentProcess.child),
|
|
251
|
+
redialAsync: async () => {
|
|
252
|
+
connectionConfig = await dialAsync();
|
|
253
|
+
return connectionConfig;
|
|
254
|
+
},
|
|
255
|
+
stopAsync,
|
|
256
|
+
};
|
|
257
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@expo/build-tools",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "23.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>",
|
|
@@ -39,17 +39,17 @@
|
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"@expo/config": "55.0.10",
|
|
41
41
|
"@expo/config-plugins": "55.0.7",
|
|
42
|
-
"@expo/downloader": "
|
|
43
|
-
"@expo/eas-build-job": "
|
|
42
|
+
"@expo/downloader": "23.0.0",
|
|
43
|
+
"@expo/eas-build-job": "23.0.0",
|
|
44
44
|
"@expo/env": "^0.4.0",
|
|
45
|
-
"@expo/logger": "
|
|
45
|
+
"@expo/logger": "23.0.0",
|
|
46
46
|
"@expo/package-manager": "1.9.10",
|
|
47
47
|
"@expo/plist": "^0.3.5",
|
|
48
48
|
"@expo/results": "^1.0.0",
|
|
49
49
|
"@expo/spawn-async": "1.7.2",
|
|
50
|
-
"@expo/steps": "
|
|
51
|
-
"@expo/template-file": "
|
|
52
|
-
"@expo/turtle-spawn": "
|
|
50
|
+
"@expo/steps": "23.0.0",
|
|
51
|
+
"@expo/template-file": "23.0.0",
|
|
52
|
+
"@expo/turtle-spawn": "23.0.0",
|
|
53
53
|
"@expo/xcpretty": "^4.3.1",
|
|
54
54
|
"@ngrok/ngrok": "1.7.0",
|
|
55
55
|
"@sentry/node": "7.77.0",
|
|
@@ -103,5 +103,5 @@
|
|
|
103
103
|
"typescript": "^5.5.4",
|
|
104
104
|
"uuid": "^9.0.1"
|
|
105
105
|
},
|
|
106
|
-
"gitHead": "
|
|
106
|
+
"gitHead": "2e2aaa4ac902b653934d7dccdc0c9b72cb7b9cac"
|
|
107
107
|
}
|