@expo/build-tools 22.2.0 → 22.3.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.
@@ -81,7 +81,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
81
81
  await (0, restoreBuildCache_1.restoreCcacheAsync)({
82
82
  logger: ctx.logger,
83
83
  workingDirectory,
84
- platform: ctx.job.platform,
84
+ target: { platform: ctx.job.platform },
85
85
  env: ctx.env,
86
86
  secrets: ctx.job.secrets,
87
87
  });
@@ -203,7 +203,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
203
203
  await (0, saveBuildCache_1.saveCcacheAsync)({
204
204
  logger: ctx.logger,
205
205
  workingDirectory,
206
- platform: ctx.job.platform,
206
+ target: { platform: ctx.job.platform },
207
207
  evictUsedBefore,
208
208
  env: ctx.env,
209
209
  secrets: ctx.job.secrets,
@@ -97,7 +97,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
97
97
  await (0, restoreBuildCache_1.restoreCcacheAsync)({
98
98
  logger: ctx.logger,
99
99
  workingDirectory,
100
- platform: ctx.job.platform,
100
+ target: { platform: ctx.job.platform, simulator: ctx.job.simulator === true },
101
101
  env: ctx.env,
102
102
  secrets: ctx.job.secrets,
103
103
  });
@@ -224,7 +224,7 @@ async function buildInnerAsync(ctx, jobHooksRef) {
224
224
  await (0, saveBuildCache_1.saveCcacheAsync)({
225
225
  logger: ctx.logger,
226
226
  workingDirectory,
227
- platform: ctx.job.platform,
227
+ target: { platform: ctx.job.platform, simulator: ctx.job.simulator === true },
228
228
  evictUsedBefore,
229
229
  env: ctx.env,
230
230
  secrets: ctx.job.secrets,
@@ -20,8 +20,10 @@ const generateGymfileFromTemplate_1 = require("./functions/generateGymfileFromTe
20
20
  const getCredentialsForBuildTriggeredByGitHubIntegration_1 = require("./functions/getCredentialsForBuildTriggeredByGitHubIntegration");
21
21
  const injectAndroidCredentials_1 = require("./functions/injectAndroidCredentials");
22
22
  const installMaestro_1 = require("./functions/installMaestro");
23
+ const installBuild_1 = require("./functions/installBuild");
23
24
  const installNodeModules_1 = require("./functions/installNodeModules");
24
25
  const installPods_1 = require("./functions/installPods");
26
+ const launchApplication_1 = require("./functions/launchApplication");
25
27
  const prebuild_1 = require("./functions/prebuild");
26
28
  const readAppConfig_1 = require("./functions/readAppConfig");
27
29
  const readIpaInfo_1 = require("./functions/readIpaInfo");
@@ -70,6 +72,8 @@ function getEasFunctions(ctx) {
70
72
  (0, prebuild_1.createPrebuildBuildFunction)(),
71
73
  (0, readIpaInfo_1.createReadIpaInfoBuildFunction)(),
72
74
  (0, downloadBuild_1.createDownloadBuildFunction)(ctx),
75
+ (0, installBuild_1.createInstallBuildFunction)(),
76
+ (0, launchApplication_1.createLaunchApplicationFunction)(),
73
77
  (0, export_1.createEasExportBuildFunction)(),
74
78
  (0, deploy_1.createEasDeployBuildFunction)(),
75
79
  (0, repack_1.createRepackBuildFunction)(),
@@ -2,13 +2,20 @@ import { bunyan } from '@expo/logger';
2
2
  import { BuildFunction } from '@expo/steps';
3
3
  import { Client } from '@urql/core';
4
4
  import { CustomBuildContext } from '../../customBuildContext';
5
+ type DownloadBuildSource = {
6
+ buildId: string;
7
+ applicationArchiveUrl?: never;
8
+ } | {
9
+ buildId?: never;
10
+ applicationArchiveUrl: string;
11
+ };
5
12
  export declare function createDownloadBuildFunction(ctx: CustomBuildContext): BuildFunction;
6
- export declare function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }: {
13
+ export declare function downloadBuildAsync(params: DownloadBuildSource & {
7
14
  logger: bunyan;
8
- buildId: string;
9
15
  graphqlClient: Client;
10
16
  robotAccessToken: string | null;
11
17
  extensions: string[];
12
18
  }): Promise<{
13
19
  artifactPath: string;
14
20
  }>;
21
+ export {};
@@ -8,9 +8,11 @@ exports.downloadBuildAsync = downloadBuildAsync;
8
8
  const eas_build_job_1 = require("@expo/eas-build-job");
9
9
  const results_1 = require("@expo/results");
10
10
  const steps_1 = require("@expo/steps");
11
+ const content_disposition_1 = __importDefault(require("content-disposition"));
11
12
  const fast_glob_1 = require("fast-glob");
12
13
  const gql_tada_1 = require("gql.tada");
13
14
  const node_fetch_1 = __importDefault(require("node-fetch"));
15
+ const node_assert_1 = __importDefault(require("node:assert"));
14
16
  const node_fs_1 = __importDefault(require("node:fs"));
15
17
  const node_os_1 = __importDefault(require("node:os"));
16
18
  const node_path_1 = __importDefault(require("node:path"));
@@ -44,7 +46,12 @@ function createDownloadBuildFunction(ctx) {
44
46
  inputProviders: [
45
47
  steps_1.BuildStepInput.createProvider({
46
48
  id: 'build_id',
47
- required: true,
49
+ required: false,
50
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
51
+ }),
52
+ steps_1.BuildStepInput.createProvider({
53
+ id: 'application_archive_url',
54
+ required: false,
48
55
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
49
56
  }),
50
57
  steps_1.BuildStepInput.createProvider({
@@ -64,11 +71,29 @@ function createDownloadBuildFunction(ctx) {
64
71
  const { logger } = stepsCtx;
65
72
  const extensions = zod_1.z.array(zod_1.z.string()).parse(inputs.extensions.value);
66
73
  logger.info(`Expected extensions: [${extensions.join(', ')}]`);
67
- const buildId = zod_1.z.string().uuid().parse(inputs.build_id.value);
68
- logger.info(`Downloading build ${buildId}...`);
74
+ const buildId = inputs.build_id.value
75
+ ? zod_1.z.string().uuid().parse(inputs.build_id.value)
76
+ : undefined;
77
+ const applicationArchiveUrl = inputs.application_archive_url.value
78
+ ? parseHttpApplicationArchiveUrl(inputs.application_archive_url.value)
79
+ : undefined;
80
+ let source;
81
+ if (buildId) {
82
+ if (applicationArchiveUrl) {
83
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of build_id or application_archive_url.');
84
+ }
85
+ source = { buildId };
86
+ }
87
+ else {
88
+ if (!applicationArchiveUrl) {
89
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass build_id or application_archive_url.');
90
+ }
91
+ source = { applicationArchiveUrl };
92
+ }
93
+ logger.info(buildId ? `Downloading build ${buildId}...` : `Downloading application archive...`);
69
94
  const { artifactPath } = await downloadBuildAsync({
70
95
  logger,
71
- buildId,
96
+ ...source,
72
97
  graphqlClient: ctx.graphqlClient,
73
98
  robotAccessToken: stepsCtx.global.staticContext.job.secrets?.robotAccessToken ?? null,
74
99
  extensions,
@@ -92,21 +117,32 @@ async function fetchApplicationArchiveUrlAsync({ buildId, graphqlClient, }) {
92
117
  }
93
118
  return applicationArchiveUrl;
94
119
  }
95
- async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessToken, extensions, }) {
120
+ async function downloadBuildAsync(params) {
121
+ const { logger, graphqlClient, robotAccessToken, extensions } = params;
122
+ let downloadUrl;
123
+ let headers;
124
+ if (params.applicationArchiveUrl) {
125
+ if (params.buildId) {
126
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass only one of buildId or applicationArchiveUrl.');
127
+ }
128
+ downloadUrl = parseHttpApplicationArchiveUrl(params.applicationArchiveUrl);
129
+ headers = undefined;
130
+ }
131
+ else if (params.buildId) {
132
+ const buildId = zod_1.z.string().uuid().parse(params.buildId);
133
+ downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
134
+ headers = robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined;
135
+ }
136
+ else {
137
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_SOURCE', 'Pass buildId or applicationArchiveUrl.');
138
+ }
96
139
  const downloadDestinationDirectory = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), 'download_build-downloaded-'));
97
- const downloadUrl = await fetchApplicationArchiveUrlAsync({ buildId, graphqlClient });
98
- const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, {
99
- headers: robotAccessToken ? { Authorization: `Bearer ${robotAccessToken}` } : undefined,
100
- });
140
+ const response = await (0, retryOnDNSFailure_1.retryOnDNSFailure)(node_fetch_1.default)(downloadUrl, { headers });
101
141
  if (!response.ok) {
102
142
  const textResult = await (0, results_1.asyncResult)(response.text());
103
143
  throw new Error(`Unexpected response from server (${response.status}): ${textResult.value}`);
104
144
  }
105
- // URL may contain percent-encoded characters, e.g. my%20file.apk
106
- // this replaces all non-alphanumeric characters (excluding dot) with underscore
107
- const archiveFilename = node_path_1.default
108
- .basename(new URL(response.url).pathname)
109
- .replace(/([^a-z0-9.-]+)/gi, '_');
145
+ const archiveFilename = resolveArchiveFilename({ response, extensions });
110
146
  const archivePath = node_path_1.default.join(downloadDestinationDirectory, archiveFilename);
111
147
  await streamPipeline(response.body, node_fs_1.default.createWriteStream(archivePath));
112
148
  const { size } = await node_fs_1.default.promises.stat(archivePath);
@@ -133,3 +169,36 @@ async function downloadBuildAsync({ logger, buildId, graphqlClient, robotAccessT
133
169
  logger.info(`Found ${matchingFiles.length} matching ${(0, strings_1.pluralize)(matchingFiles.length, 'entry')}:\n${matchingFiles.map(f => `- ${node_path_1.default.relative(extractionDirectory, f)}`).join('\n')}`);
134
170
  return { artifactPath: matchingFiles[0] };
135
171
  }
172
+ function parseHttpApplicationArchiveUrl(value) {
173
+ try {
174
+ const applicationArchiveUrl = zod_1.z.string().parse(value);
175
+ const parsedUrl = new URL(applicationArchiveUrl);
176
+ (0, node_assert_1.default)(parsedUrl.protocol === 'http:' || parsedUrl.protocol === 'https:');
177
+ return applicationArchiveUrl;
178
+ }
179
+ catch {
180
+ throw new eas_build_job_1.UserError('EAS_DOWNLOAD_BUILD_INVALID_APPLICATION_ARCHIVE_URL', 'application_archive_url must be a valid HTTP or HTTPS URL.');
181
+ }
182
+ }
183
+ function resolveArchiveFilename({ response, extensions, }) {
184
+ const contentDispositionHeader = response.headers.get('content-disposition');
185
+ let headerFilename;
186
+ if (contentDispositionHeader) {
187
+ try {
188
+ headerFilename = content_disposition_1.default.parse(contentDispositionHeader).parameters.filename;
189
+ }
190
+ catch {
191
+ // Ignore malformed Content-Disposition headers and fall back to the response URL.
192
+ }
193
+ }
194
+ const urlFilename = node_path_1.default.basename(new URL(response.url).pathname);
195
+ let archiveFilename = node_path_1.default.basename(headerFilename ?? urlFilename ?? '');
196
+ if (!archiveFilename || archiveFilename === '.' || archiveFilename === '..') {
197
+ archiveFilename = 'application';
198
+ }
199
+ if (!node_path_1.default.extname(archiveFilename) && extensions.length === 1) {
200
+ archiveFilename = `${archiveFilename}.${extensions[0]}`;
201
+ }
202
+ // URL and header filenames may contain percent-encoded or unsafe filesystem characters.
203
+ return archiveFilename.replace(/([^a-z0-9.-]+)/gi, '_');
204
+ }
@@ -0,0 +1,12 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
3
+ export declare function createInstallBuildFunction(): BuildFunction;
4
+ export declare function installBuildAsync({ artifactPath, runtimePlatform, env, logger, }: {
5
+ artifactPath: string;
6
+ runtimePlatform: BuildRuntimePlatform;
7
+ env: BuildStepEnv;
8
+ logger: bunyan;
9
+ }): Promise<{
10
+ applicationIdentifier: string;
11
+ activityName?: string;
12
+ }>;
@@ -0,0 +1,85 @@
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.createInstallBuildFunction = createInstallBuildFunction;
7
+ exports.installBuildAsync = installBuildAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
+ const steps_1 = require("@expo/steps");
10
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ const node_fs_1 = __importDefault(require("node:fs"));
12
+ const node_path_1 = __importDefault(require("node:path"));
13
+ const zod_1 = require("zod");
14
+ function createInstallBuildFunction() {
15
+ return new steps_1.BuildFunction({
16
+ namespace: 'eas',
17
+ id: 'install_build',
18
+ name: 'Install build',
19
+ __metricsId: 'eas/install_build',
20
+ inputProviders: [
21
+ steps_1.BuildStepInput.createProvider({
22
+ id: 'artifact_path',
23
+ required: true,
24
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
25
+ }),
26
+ ],
27
+ outputProviders: [
28
+ steps_1.BuildStepOutput.createProvider({
29
+ id: 'application_identifier',
30
+ required: true,
31
+ }),
32
+ steps_1.BuildStepOutput.createProvider({
33
+ id: 'activity_name',
34
+ required: false,
35
+ }),
36
+ ],
37
+ fn: async ({ global, logger }, { inputs, outputs, env }) => {
38
+ const artifactPath = zod_1.z.string().min(1).parse(inputs.artifact_path.value);
39
+ const { applicationIdentifier, activityName } = await installBuildAsync({
40
+ artifactPath,
41
+ runtimePlatform: global.runtimePlatform,
42
+ env,
43
+ logger,
44
+ });
45
+ outputs.application_identifier.set(applicationIdentifier);
46
+ if (activityName) {
47
+ outputs.activity_name.set(activityName);
48
+ }
49
+ },
50
+ });
51
+ }
52
+ async function installBuildAsync({ artifactPath, runtimePlatform, env, logger, }) {
53
+ const artifactStat = await node_fs_1.default.promises.stat(artifactPath).catch(err => {
54
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', `Build artifact does not exist at ${artifactPath}.`, { cause: err });
55
+ });
56
+ if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
57
+ if (node_path_1.default.extname(artifactPath) !== '.app' || !artifactStat.isDirectory()) {
58
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', 'iOS Simulator sessions require a .app build artifact.');
59
+ }
60
+ const infoPlistPath = node_path_1.default.join(artifactPath, 'Info.plist');
61
+ const { stdout } = await (0, turtle_spawn_1.default)('plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', infoPlistPath], { stdio: 'pipe', env });
62
+ const applicationIdentifier = stdout.trim();
63
+ if (!applicationIdentifier) {
64
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_MISSING_IDENTIFIER', `Could not read CFBundleIdentifier from ${infoPlistPath}.`);
65
+ }
66
+ logger.info(`Installing ${artifactPath} on the iOS Simulator.`);
67
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'install', 'booted', artifactPath], { env, logger });
68
+ return { applicationIdentifier };
69
+ }
70
+ if (node_path_1.default.extname(artifactPath) !== '.apk' || !artifactStat.isFile()) {
71
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_INVALID_ARTIFACT', 'Android Emulator sessions require an .apk build artifact.');
72
+ }
73
+ const { stdout } = await (0, turtle_spawn_1.default)('aapt2', ['dump', 'badging', artifactPath], {
74
+ stdio: 'pipe',
75
+ env,
76
+ });
77
+ const applicationIdentifier = stdout.match(/package: name='([^']+)'/)?.[1];
78
+ const activityName = stdout.match(/launchable-activity: name='([^']+)'/)?.[1];
79
+ if (!applicationIdentifier) {
80
+ throw new eas_build_job_1.UserError('EAS_INSTALL_BUILD_MISSING_IDENTIFIER', `Could not read an Android application identifier from ${artifactPath}.`);
81
+ }
82
+ logger.info(`Installing ${artifactPath} on the Android Emulator.`);
83
+ await (0, turtle_spawn_1.default)('adb', ['install', '-r', artifactPath], { env, logger });
84
+ return { applicationIdentifier, activityName };
85
+ }
@@ -0,0 +1,10 @@
1
+ import { bunyan } from '@expo/logger';
2
+ import { BuildFunction, BuildRuntimePlatform, BuildStepEnv } from '@expo/steps';
3
+ export declare function createLaunchApplicationFunction(): BuildFunction;
4
+ export declare function launchApplicationAsync({ applicationIdentifier, activityName, runtimePlatform, env, logger, }: {
5
+ applicationIdentifier: string;
6
+ activityName?: string;
7
+ runtimePlatform: BuildRuntimePlatform;
8
+ env: BuildStepEnv;
9
+ logger: bunyan;
10
+ }): Promise<void>;
@@ -0,0 +1,67 @@
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.createLaunchApplicationFunction = createLaunchApplicationFunction;
7
+ exports.launchApplicationAsync = launchApplicationAsync;
8
+ const eas_build_job_1 = require("@expo/eas-build-job");
9
+ const steps_1 = require("@expo/steps");
10
+ const turtle_spawn_1 = __importDefault(require("@expo/turtle-spawn"));
11
+ function createLaunchApplicationFunction() {
12
+ return new steps_1.BuildFunction({
13
+ namespace: 'eas',
14
+ id: 'launch_application',
15
+ name: 'Launch application',
16
+ __metricsId: 'eas/launch_application',
17
+ inputProviders: [
18
+ steps_1.BuildStepInput.createProvider({
19
+ id: 'application_identifier',
20
+ required: true,
21
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
22
+ }),
23
+ steps_1.BuildStepInput.createProvider({
24
+ id: 'activity_name',
25
+ required: false,
26
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
27
+ }),
28
+ ],
29
+ fn: async ({ global, logger }, { inputs, env }) => {
30
+ const applicationIdentifier = parseNonEmptyStringInput(inputs.application_identifier.value, 'application_identifier');
31
+ const activityName = inputs.activity_name.value === undefined
32
+ ? undefined
33
+ : parseNonEmptyStringInput(inputs.activity_name.value, 'activity_name');
34
+ await launchApplicationAsync({
35
+ applicationIdentifier,
36
+ activityName,
37
+ runtimePlatform: global.runtimePlatform,
38
+ env,
39
+ logger,
40
+ });
41
+ },
42
+ });
43
+ }
44
+ async function launchApplicationAsync({ applicationIdentifier, activityName, runtimePlatform, env, logger, }) {
45
+ if (runtimePlatform === steps_1.BuildRuntimePlatform.DARWIN) {
46
+ logger.info(`Launching ${applicationIdentifier}.`);
47
+ await (0, turtle_spawn_1.default)('xcrun', ['simctl', 'launch', 'booted', applicationIdentifier], {
48
+ env,
49
+ logger,
50
+ });
51
+ return;
52
+ }
53
+ if (!activityName) {
54
+ throw new eas_build_job_1.UserError('EAS_LAUNCH_APPLICATION_MISSING_ACTIVITY', 'Launching an Android application requires activity_name.');
55
+ }
56
+ logger.info(`Launching ${applicationIdentifier}.`);
57
+ await (0, turtle_spawn_1.default)('adb', ['shell', 'am', 'start', '-n', `${applicationIdentifier}/${activityName}`], {
58
+ env,
59
+ logger,
60
+ });
61
+ }
62
+ function parseNonEmptyStringInput(value, inputName) {
63
+ if (typeof value !== 'string' || value.length === 0) {
64
+ throw new eas_build_job_1.UserError('EAS_LAUNCH_APPLICATION_INVALID_INPUT', `Input "${inputName}" must be a non-empty string. Pass the "${inputName}" output from eas/install_build.`);
65
+ }
66
+ return value;
67
+ }
@@ -3,6 +3,7 @@ export type IpaInfo = {
3
3
  bundleIdentifier: string;
4
4
  bundleShortVersion: string;
5
5
  bundleVersion: string;
6
+ dtPlatformName: string | null;
6
7
  };
7
8
  export declare function createReadIpaInfoBuildFunction(): BuildFunction;
8
9
  export declare function readIpaInfoAsync(ipaPath: string): Promise<IpaInfo>;
@@ -70,10 +70,12 @@ async function readIpaInfoAsync(ipaPath) {
70
70
  if (typeof bundleVersion !== 'string') {
71
71
  throw new eas_build_job_1.UserError('EAS_READ_IPA_INFO_INVALID_INFO_PLIST', 'Failed to read IPA info: Missing or invalid CFBundleVersion in Info.plist');
72
72
  }
73
+ const dtPlatformName = typeof infoPlist.DTPlatformName === 'string' ? infoPlist.DTPlatformName : null;
73
74
  return {
74
75
  bundleIdentifier,
75
76
  bundleShortVersion,
76
77
  bundleVersion,
78
+ dtPlatformName,
77
79
  };
78
80
  }
79
81
  catch (error) {
@@ -11,11 +11,14 @@ export declare function resolveAndroidSigningOptionsAsync({ job, tmpDir, }: {
11
11
  tmpDir: string;
12
12
  }): Promise<AndroidSigningOptions | undefined>;
13
13
  /**
14
- * Resolves iOS signing options from the job secrets.
14
+ * Resolves iOS signing options from the job secrets, dispatching on the
15
+ * requested signing backend.
15
16
  */
16
- export declare function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements, entitlementsPath, }: {
17
+ export declare function resolveIosSigningOptionsAsync({ job, logger, backend, useAppEntitlements, entitlementsPath, tmpDir, }: {
17
18
  job: Job;
18
19
  logger: bunyan;
20
+ backend?: 'fastlane' | 'zsign';
19
21
  useAppEntitlements?: boolean;
20
22
  entitlementsPath?: string;
23
+ tmpDir: string;
21
24
  }): Promise<IosSigningOptions | undefined>;
@@ -53,6 +53,12 @@ function createRepackBuildFunction() {
53
53
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
54
54
  required: false,
55
55
  }),
56
+ steps_1.BuildStepInput.createProvider({
57
+ id: 'ios_signing_backend',
58
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
59
+ required: false,
60
+ allowedValues: ['fastlane', 'zsign'],
61
+ }),
56
62
  steps_1.BuildStepInput.createProvider({
57
63
  id: 'repack_version',
58
64
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
@@ -123,8 +129,10 @@ function createRepackBuildFunction() {
123
129
  iosSigningOptions: await resolveIosSigningOptionsAsync({
124
130
  job: stepsCtx.global.staticContext.job,
125
131
  logger: stepsCtx.logger,
132
+ backend: inputs.ios_signing_backend.value,
126
133
  useAppEntitlements: inputs.ios_signing_use_source_app_entitlements.value,
127
134
  entitlementsPath: inputs.ios_signing_app_entitlements_path.value,
135
+ tmpDir,
128
136
  }),
129
137
  logger: stepsCtx.logger,
130
138
  spawnAsync: repackSpawnAsync,
@@ -234,14 +242,26 @@ async function resolveAndroidSigningOptionsAsync({ job, tmpDir, }) {
234
242
  };
235
243
  }
236
244
  /**
237
- * Resolves iOS signing options from the job secrets.
245
+ * Resolves iOS signing options from the job secrets, dispatching on the
246
+ * requested signing backend.
238
247
  */
239
- async function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements, entitlementsPath, }) {
248
+ async function resolveIosSigningOptionsAsync({ job, logger, backend, useAppEntitlements, entitlementsPath, tmpDir, }) {
240
249
  const iosJob = job;
241
250
  const buildCredentials = iosJob.secrets?.buildCredentials;
242
251
  if (iosJob.simulator || buildCredentials == null) {
243
252
  return undefined;
244
253
  }
254
+ const commonOptions = { buildCredentials, logger, useAppEntitlements, entitlementsPath };
255
+ return backend === 'zsign'
256
+ ? await createIosZsignOptionsAsync({ ...commonOptions, tmpDir })
257
+ : await createIosFastlaneOptionsAsync(commonOptions);
258
+ }
259
+ /**
260
+ * Creates signing options for the fastlane backend: certificates are imported
261
+ * into a temporary keychain and provisioning profiles are parsed with the
262
+ * macOS `security` tool.
263
+ */
264
+ async function createIosFastlaneOptionsAsync({ buildCredentials, logger, useAppEntitlements, entitlementsPath, }) {
245
265
  const credentialsManager = new manager_1.default(buildCredentials);
246
266
  const credentials = await credentialsManager.prepare(logger);
247
267
  const provisioningProfile = {};
@@ -256,3 +276,31 @@ async function resolveIosSigningOptionsAsync({ job, logger, useAppEntitlements,
256
276
  entitlementsPath,
257
277
  };
258
278
  }
279
+ /**
280
+ * Creates signing options for the zsign backend. The distribution certificate
281
+ * secret is already a PKCS#12 file, so it goes to disk as-is together with the
282
+ * provisioning profiles.
283
+ */
284
+ async function createIosZsignOptionsAsync({ buildCredentials, logger, useAppEntitlements, entitlementsPath, tmpDir, }) {
285
+ const targets = Object.entries(buildCredentials);
286
+ const [targetName, targetCredentials] = targets[0];
287
+ logger.info(`Using the distribution certificate from target '${targetName}' for zsign`);
288
+ const certificatePath = node_path_1.default.join(tmpDir, `dist-cert-${(0, node_crypto_1.randomUUID)()}.p12`);
289
+ await node_fs_1.default.promises.writeFile(certificatePath, new Uint8Array(Buffer.from(targetCredentials.distributionCertificate.dataBase64, 'base64')));
290
+ // zsign matches profiles to bundles by the app-id suffix itself, so the
291
+ // record keys are informational only.
292
+ const provisioningProfile = {};
293
+ for (const [target, credentials] of targets) {
294
+ const profilePath = node_path_1.default.join(tmpDir, `profile-${target}-${(0, node_crypto_1.randomUUID)()}.mobileprovision`);
295
+ await node_fs_1.default.promises.writeFile(profilePath, new Uint8Array(Buffer.from(credentials.provisioningProfileBase64, 'base64')));
296
+ provisioningProfile[target] = profilePath;
297
+ }
298
+ return {
299
+ backend: 'zsign',
300
+ certificatePath,
301
+ keyPassword: targetCredentials.distributionCertificate.password,
302
+ provisioningProfile,
303
+ useAppEntitlements,
304
+ entitlementsPath,
305
+ };
306
+ }
@@ -1,12 +1,12 @@
1
- import { Platform } from '@expo/eas-build-job';
2
1
  import { bunyan } from '@expo/logger';
3
2
  import { BuildFunction } from '@expo/steps';
3
+ import { CcacheBuildTarget } from '../../utils/cacheKey';
4
4
  export declare function createRestoreBuildCacheFunction(): BuildFunction;
5
5
  export declare function createCacheStatsBuildFunction(): BuildFunction;
6
- export declare function restoreCcacheAsync({ logger, workingDirectory, platform, env, secrets, }: {
6
+ export declare function restoreCcacheAsync({ logger, workingDirectory, target, env, secrets, }: {
7
7
  logger: bunyan;
8
8
  workingDirectory: string;
9
- platform: Platform;
9
+ target: CcacheBuildTarget;
10
10
  env: Record<string, string | undefined>;
11
11
  secrets?: {
12
12
  robotAccessToken?: string;
@@ -33,6 +33,11 @@ function createRestoreBuildCacheFunction() {
33
33
  required: false,
34
34
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
35
35
  }),
36
+ steps_1.BuildStepInput.createProvider({
37
+ id: 'simulator',
38
+ required: false,
39
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
40
+ }),
36
41
  ],
37
42
  fn: async (stepCtx, { env, inputs }) => {
38
43
  const { logger } = stepCtx;
@@ -42,10 +47,18 @@ function createRestoreBuildCacheFunction() {
42
47
  if (!platform || ![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
43
48
  throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
44
49
  }
50
+ const target = platform === eas_build_job_1.Platform.IOS
51
+ ? {
52
+ platform,
53
+ simulator: inputs.simulator.value ??
54
+ (stepCtx.global.staticContext.job.platform === eas_build_job_1.Platform.IOS &&
55
+ stepCtx.global.staticContext.job.simulator === true),
56
+ }
57
+ : { platform };
45
58
  await restoreCcacheAsync({
46
59
  logger,
47
60
  workingDirectory,
48
- platform,
61
+ target,
49
62
  env,
50
63
  secrets: stepCtx.global.staticContext.job.secrets,
51
64
  });
@@ -80,7 +93,7 @@ function createCacheStatsBuildFunction() {
80
93
  },
81
94
  });
82
95
  }
83
- async function restoreCcacheAsync({ logger, workingDirectory, platform, env, secrets, }) {
96
+ async function restoreCcacheAsync({ logger, workingDirectory, target, env, secrets, }) {
84
97
  const enabled = env.EAS_RESTORE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_RESTORE_CACHE !== '0');
85
98
  if (!enabled) {
86
99
  return;
@@ -103,7 +116,7 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
103
116
  env,
104
117
  stdio: 'pipe',
105
118
  }));
106
- const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, platform);
119
+ const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, target);
107
120
  logger.info(`Restoring cache key: ${cacheKey}`);
108
121
  const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
109
122
  const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
@@ -113,8 +126,8 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
113
126
  robotAccessToken,
114
127
  paths: [cachePath],
115
128
  key: cacheKey,
116
- keyPrefixes: [cacheKey_1.CACHE_KEY_PREFIX_BY_PLATFORM[platform]],
117
- platform,
129
+ keyPrefixes: [(0, cacheKey_1.getCcacheKeyPrefix)(target)],
130
+ platform: target.platform,
118
131
  });
119
132
  await (0, restoreCache_1.decompressCacheAsync)({
120
133
  archivePath,
@@ -139,7 +152,7 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
139
152
  expoApiServerURL,
140
153
  robotAccessToken,
141
154
  paths: [cachePath],
142
- platform,
155
+ platform: target.platform,
143
156
  });
144
157
  await (0, restoreCache_1.decompressCacheAsync)({
145
158
  archivePath,
@@ -1,11 +1,11 @@
1
- import { Platform } from '@expo/eas-build-job';
2
1
  import { bunyan } from '@expo/logger';
3
2
  import { BuildFunction } from '@expo/steps';
3
+ import { CcacheBuildTarget } from '../../utils/cacheKey';
4
4
  export declare function createSaveBuildCacheFunction(evictUsedBefore: Date): BuildFunction;
5
- export declare function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBefore, env, secrets, }: {
5
+ export declare function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }: {
6
6
  logger: bunyan;
7
7
  workingDirectory: string;
8
- platform: Platform;
8
+ target: CcacheBuildTarget;
9
9
  evictUsedBefore: Date;
10
10
  env: Record<string, string | undefined>;
11
11
  secrets?: {
@@ -29,6 +29,11 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
29
29
  required: false,
30
30
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
31
31
  }),
32
+ steps_1.BuildStepInput.createProvider({
33
+ id: 'simulator',
34
+ required: false,
35
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
36
+ }),
32
37
  ],
33
38
  fn: async (stepCtx, { env, inputs }) => {
34
39
  const { logger } = stepCtx;
@@ -38,10 +43,18 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
38
43
  if (!platform || ![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
39
44
  throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
40
45
  }
46
+ const target = platform === eas_build_job_1.Platform.IOS
47
+ ? {
48
+ platform,
49
+ simulator: inputs.simulator.value ??
50
+ (stepCtx.global.staticContext.job.platform === eas_build_job_1.Platform.IOS &&
51
+ stepCtx.global.staticContext.job.simulator === true),
52
+ }
53
+ : { platform };
41
54
  await saveCcacheAsync({
42
55
  logger,
43
56
  workingDirectory,
44
- platform,
57
+ target,
45
58
  evictUsedBefore,
46
59
  env,
47
60
  secrets: stepCtx.global.staticContext.job.secrets,
@@ -57,7 +70,7 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
57
70
  },
58
71
  });
59
72
  }
60
- async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBefore, env, secrets, }) {
73
+ async function saveCcacheAsync({ logger, workingDirectory, target, evictUsedBefore, env, secrets, }) {
61
74
  const enabled = env.EAS_SAVE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_SAVE_CACHE !== '0');
62
75
  if (!enabled) {
63
76
  return;
@@ -72,7 +85,7 @@ async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBe
72
85
  return;
73
86
  }
74
87
  try {
75
- const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, platform);
88
+ const cacheKey = await (0, cacheKey_1.generateDefaultBuildCacheKeyAsync)(workingDirectory, target);
76
89
  logger.info(`Saving cache key: ${cacheKey}`);
77
90
  const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
78
91
  const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
@@ -104,7 +117,7 @@ async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBe
104
117
  key: cacheKey,
105
118
  paths: [cachePath],
106
119
  size,
107
- platform,
120
+ platform: target.platform,
108
121
  });
109
122
  }
110
123
  catch (err) {
@@ -12,6 +12,11 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
12
12
  __metricsId: 'eas/start_serve_sim_remote_session',
13
13
  supportedRuntimePlatforms: [steps_1.BuildRuntimePlatform.DARWIN],
14
14
  inputProviders: [
15
+ steps_1.BuildStepInput.createProvider({
16
+ id: 'package_version',
17
+ required: false,
18
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
19
+ }),
15
20
  steps_1.BuildStepInput.createProvider({
16
21
  id: 'max_duration_seconds',
17
22
  required: false,
@@ -22,6 +27,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
22
27
  const deviceRunSessionId = (0, remoteDeviceRunSession_1.getDeviceRunSessionIdOrThrow)(env);
23
28
  const ngrokTunnelDomain = (0, remoteDeviceRunSession_1.getNgrokTunnelDomainOrThrow)(env);
24
29
  const maxDurationSeconds = inputs.max_duration_seconds?.value;
30
+ const packageVersion = inputs.package_version?.value;
25
31
  logger.info('Starting serve-sim remote session.');
26
32
  await (0, remoteDeviceRunSession_1.selectXcodeDeveloperDirectoryAsync)({ env, logger });
27
33
  const serveSim = await (0, remoteDeviceRunSession_1.startServeSimWithTunnelAsync)(ctx, {
@@ -29,6 +35,7 @@ function createStartServeSimRemoteSessionBuildFunction(ctx) {
29
35
  env,
30
36
  logger,
31
37
  timeoutMs: STARTUP_TIMEOUT_MS,
38
+ packageVersion,
32
39
  });
33
40
  logger.info(`Preview URL: ${serveSim.previewUrl}`);
34
41
  try {
@@ -141,15 +141,25 @@ function createUploadToAscBuildFunction() {
141
141
  const appResponse = await AscApiUtils_1.AscApiUtils.getAppInfoAsync({ client, appleAppIdentifier });
142
142
  const ascAppBundleIdentifier = appResponse.data.attributes.bundleId;
143
143
  stepsCtx.logger.info(`Uploading Build to "${appResponse.data.attributes.name}" (${ascAppBundleIdentifier})...`);
144
+ // Derive the App Store Connect platform from the IPA itself so tvOS (and
145
+ // other) binaries are uploaded to the correct version train instead of the
146
+ // iOS one. Falls back to `IOS` if the platform cannot be read.
147
+ const ipaInfoResult = await (0, results_1.asyncResult)((0, readIpaInfo_1.readIpaInfoAsync)(ipaPath));
148
+ const platform = ipaInfoResult.ok
149
+ ? AscApiUtils_1.AscApiUtils.ascPlatformFromDtPlatformName(ipaInfoResult.value.dtPlatformName)
150
+ : 'IOS';
151
+ stepsCtx.logger.info(`Detected App Store Connect platform: ${platform}`);
144
152
  stepsCtx.logger.info('Creating Build Upload...');
145
153
  const buildUploadResponse = await AscApiUtils_1.AscApiUtils.createBuildUploadAsync({
146
154
  client,
147
155
  appleAppIdentifier,
148
156
  bundleShortVersion,
149
157
  bundleVersion,
158
+ platform,
150
159
  });
151
160
  const buildUploadId = buildUploadResponse.data.id;
152
- const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/ios/${buildUploadId}`;
161
+ const platformPathSegment = AscApiUtils_1.AscApiUtils.testFlightPlatformPathSegment(platform);
162
+ const buildUploadUrl = `https://appstoreconnect.apple.com/apps/${appleAppIdentifier}/testflight/${platformPathSegment}/${buildUploadId}`;
153
163
  outputs.build_upload_id.set(buildUploadId);
154
164
  outputs.build_upload_url.set(buildUploadUrl);
155
165
  stepsCtx.logger.info(`Build Upload initialized (ID: ${buildUploadId}). Preparing IPA upload...`);
@@ -264,6 +264,7 @@ export type AscApiClientPostApi = {
264
264
  response: z.output<(typeof PostApi)[Path]['response']>;
265
265
  };
266
266
  };
267
+ export type AscPlatform = AscApiClientPostApi['/v1/buildUploads']['request']['data']['attributes']['platform'];
267
268
  export type AscApiClientPatchApi = {
268
269
  [Path in keyof typeof PatchApi]: {
269
270
  request: z.input<(typeof PatchApi)[Path]['request']>;
@@ -1,14 +1,23 @@
1
- import { AscApiClient, AscApiClientGetApi, AscApiClientPostApi } from './AscApiClient';
1
+ import { AscApiClient, AscApiClientGetApi, AscApiClientPostApi, AscPlatform } from './AscApiClient';
2
2
  export declare namespace AscApiUtils {
3
+ /**
4
+ * Maps a bundle's `DTPlatformName` (from its Info.plist) to the App Store
5
+ * Connect platform used for a build upload. Unknown or missing values fall
6
+ * back to `IOS` to preserve the previous default.
7
+ */
8
+ function ascPlatformFromDtPlatformName(dtPlatformName: string | null): AscPlatform;
9
+ /** The App Store Connect TestFlight URL path segment for a platform. */
10
+ function testFlightPlatformPathSegment(platform: AscPlatform): string;
3
11
  function getAppInfoAsync({ client, appleAppIdentifier, }: {
4
12
  client: Pick<AscApiClient, 'getAsync'>;
5
13
  appleAppIdentifier: string;
6
14
  }): Promise<AscApiClientGetApi['/v1/apps/:id']['response']>;
7
- function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }: {
15
+ function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, platform, }: {
8
16
  client: Pick<AscApiClient, 'postAsync'>;
9
17
  appleAppIdentifier: string;
10
18
  bundleShortVersion: string;
11
19
  bundleVersion: string;
20
+ platform: AscPlatform;
12
21
  }): Promise<AscApiClientPostApi['/v1/buildUploads']['response']>;
13
22
  function getAppsAsync({ client, limit, }: {
14
23
  client: Pick<AscApiClient, 'getAsync'>;
@@ -5,6 +5,40 @@ const eas_build_job_1 = require("@expo/eas-build-job");
5
5
  const AscApiClient_1 = require("./AscApiClient");
6
6
  var AscApiUtils;
7
7
  (function (AscApiUtils) {
8
+ /**
9
+ * Maps a bundle's `DTPlatformName` (from its Info.plist) to the App Store
10
+ * Connect platform used for a build upload. Unknown or missing values fall
11
+ * back to `IOS` to preserve the previous default.
12
+ */
13
+ function ascPlatformFromDtPlatformName(dtPlatformName) {
14
+ switch (dtPlatformName) {
15
+ case 'appletvos':
16
+ return 'TV_OS';
17
+ case 'macosx':
18
+ return 'MAC_OS';
19
+ case 'xros':
20
+ return 'VISION_OS';
21
+ case 'iphoneos':
22
+ default:
23
+ return 'IOS';
24
+ }
25
+ }
26
+ AscApiUtils.ascPlatformFromDtPlatformName = ascPlatformFromDtPlatformName;
27
+ /** The App Store Connect TestFlight URL path segment for a platform. */
28
+ function testFlightPlatformPathSegment(platform) {
29
+ switch (platform) {
30
+ case 'TV_OS':
31
+ return 'tvos';
32
+ case 'MAC_OS':
33
+ return 'macos';
34
+ case 'VISION_OS':
35
+ return 'visionos';
36
+ case 'IOS':
37
+ default:
38
+ return 'ios';
39
+ }
40
+ }
41
+ AscApiUtils.testFlightPlatformPathSegment = testFlightPlatformPathSegment;
8
42
  async function getAppInfoAsync({ client, appleAppIdentifier, }) {
9
43
  try {
10
44
  return await client.getAsync('/v1/apps/:id', { 'fields[apps]': ['bundleId', 'name'] }, { id: appleAppIdentifier });
@@ -37,13 +71,13 @@ var AscApiUtils;
37
71
  }
38
72
  }
39
73
  AscApiUtils.getAppInfoAsync = getAppInfoAsync;
40
- async function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, }) {
74
+ async function createBuildUploadAsync({ client, appleAppIdentifier, bundleShortVersion, bundleVersion, platform, }) {
41
75
  try {
42
76
  return await client.postAsync('/v1/buildUploads', {
43
77
  data: {
44
78
  type: 'buildUploads',
45
79
  attributes: {
46
- platform: 'IOS',
80
+ platform,
47
81
  cfBundleShortVersionString: bundleShortVersion,
48
82
  cfBundleVersion: bundleVersion,
49
83
  },
@@ -92,10 +92,11 @@ export declare function spawnDetached({ command, args, cwd, env, }: {
92
92
  env: BuildStepEnv;
93
93
  }): DetachedProcessHandle;
94
94
  export declare function metricsCorsOriginToServeSimArgs(env: BuildStepEnv): string[];
95
- export declare function createServeSimArgs({ port, turnArgs, metricsCorsArgs, }: {
95
+ export declare function createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion, }: {
96
96
  port: number;
97
97
  turnArgs?: string[];
98
98
  metricsCorsArgs?: string[];
99
+ packageVersion?: string;
99
100
  }): string[];
100
101
  export declare function waitForServeSimReadyAsync({ serveSim, port, timeoutMs, }: {
101
102
  serveSim: Pick<DetachedProcessHandle, 'pid' | 'getOutput'>;
@@ -106,11 +107,12 @@ export type ServeSimPreviewHandle = {
106
107
  previewUrl: string;
107
108
  stopAsync: () => Promise<void>;
108
109
  };
109
- export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, }: {
110
+ export declare function startServeSimWithTunnelAsync(ctx: CustomBuildContext, { baseDomain, env, logger, timeoutMs, packageVersion, }: {
110
111
  baseDomain: string;
111
112
  env: BuildStepEnv;
112
113
  logger: bunyan;
113
114
  timeoutMs: number;
115
+ packageVersion?: string;
114
116
  }): Promise<ServeSimPreviewHandle>;
115
117
  export type NgrokTunnelHandle = {
116
118
  url: string;
@@ -69,7 +69,7 @@ const sentry_1 = require("../../sentry");
69
69
  const retry_1 = require("../../utils/retry");
70
70
  const turtleFetch_1 = require("../../utils/turtleFetch");
71
71
  const XCODE_DEVELOPER_DIR = '/Applications/Xcode.app/Contents/Developer';
72
- const SERVE_SIM_PACKAGE_SPEC = '@expo/serve-sim@latest';
72
+ const SERVE_SIM_PACKAGE_NAME = '@expo/serve-sim';
73
73
  const SERVE_SIM_HOST = '127.0.0.1';
74
74
  const SERVE_SIM_MAX_DIMENSION = '1280';
75
75
  const SERVE_SIM_MJPEG_QUALITY = '0.55';
@@ -473,10 +473,13 @@ function metricsCorsOriginToServeSimArgs(env) {
473
473
  }
474
474
  return args;
475
475
  }
476
- function createServeSimArgs({ port, turnArgs = [], metricsCorsArgs = [], }) {
476
+ function createServeSimPackageSpec(packageVersion) {
477
+ return `${SERVE_SIM_PACKAGE_NAME}@${packageVersion ?? 'latest'}`;
478
+ }
479
+ function createServeSimArgs({ port, turnArgs = [], metricsCorsArgs = [], packageVersion, }) {
477
480
  return [
478
481
  '--yes',
479
- SERVE_SIM_PACKAGE_SPEC,
482
+ createServeSimPackageSpec(packageVersion),
480
483
  '--port',
481
484
  String(port),
482
485
  '--host',
@@ -540,14 +543,14 @@ async function waitForServeSimReadyAsync({ serveSim, port, timeoutMs, }) {
540
543
  }
541
544
  throw new eas_build_job_1.SystemError(`Timed out waiting for serve-sim readiness at ${readyUrl}${lastError instanceof Error ? `: ${lastError.message}` : ''}. Last output:\n${serveSim.getOutput() || '<empty>'}`);
542
545
  }
543
- async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, }) {
546
+ async function startServeSimWithTunnelAsync(ctx, { baseDomain, env, logger, timeoutMs, packageVersion, }) {
544
547
  const port = await findAvailablePortAsync();
545
- logger.info(`Launching ${SERVE_SIM_PACKAGE_SPEC} on ${SERVE_SIM_HOST}:${port}.`);
548
+ logger.info(`Launching ${createServeSimPackageSpec(packageVersion)} on ${SERVE_SIM_HOST}:${port}.`);
546
549
  const turnArgs = await fetchServeSimTurnArgsAsync(ctx, { env, logger });
547
550
  const metricsCorsArgs = metricsCorsOriginToServeSimArgs(env);
548
551
  const serveSim = spawnDetached({
549
552
  command: 'npx',
550
- args: createServeSimArgs({ port, turnArgs, metricsCorsArgs }),
553
+ args: createServeSimArgs({ port, turnArgs, metricsCorsArgs, packageVersion }),
551
554
  env,
552
555
  });
553
556
  try {
@@ -1,5 +1,11 @@
1
1
  import { Platform } from '@expo/eas-build-job';
2
- export declare const CACHE_KEY_PREFIX_BY_PLATFORM: Record<Platform, string>;
2
+ export type CcacheBuildTarget = {
3
+ platform: Platform.ANDROID;
4
+ } | {
5
+ platform: Platform.IOS;
6
+ simulator: boolean;
7
+ };
8
+ export declare function getCcacheKeyPrefix(target: CcacheBuildTarget): string;
3
9
  export declare const PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM: Record<Platform, string>;
4
10
  export declare function getCcachePath(env: Record<string, string | undefined>): string;
5
- export declare function generateDefaultBuildCacheKeyAsync(workingDirectory: string, platform: Platform): Promise<string>;
11
+ export declare function generateDefaultBuildCacheKeyAsync(workingDirectory: string, target: CcacheBuildTarget): Promise<string>;
@@ -36,7 +36,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
36
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
37
37
  };
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
- exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = exports.CACHE_KEY_PREFIX_BY_PLATFORM = void 0;
39
+ exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = void 0;
40
+ exports.getCcacheKeyPrefix = getCcacheKeyPrefix;
40
41
  exports.getCcachePath = getCcachePath;
41
42
  exports.generateDefaultBuildCacheKeyAsync = generateDefaultBuildCacheKeyAsync;
42
43
  const eas_build_job_1 = require("@expo/eas-build-job");
@@ -46,16 +47,19 @@ const assert_1 = __importDefault(require("assert"));
46
47
  const os_1 = __importDefault(require("os"));
47
48
  const path_1 = __importDefault(require("path"));
48
49
  const packageManager_1 = require("./packageManager");
49
- const IOS_CACHE_KEY_PREFIX = 'ios-ccache-';
50
+ const IOS_DEVICE_CACHE_KEY_PREFIX = 'ios-device-ccache-';
51
+ const IOS_SIMULATOR_CACHE_KEY_PREFIX = 'ios-simulator-ccache-';
50
52
  const ANDROID_CACHE_KEY_PREFIX = 'android-ccache-';
51
53
  const PUBLIC_IOS_CACHE_KEY_PREFIX = 'public-ios-ccache-';
52
54
  const PUBLIC_ANDROID_CACHE_KEY_PREFIX = 'public-android-ccache-';
53
55
  const DARWIN_CACHE_PATH = 'Library/Caches/ccache';
54
56
  const LINUX_CACHE_PATH = '.cache/ccache';
55
- exports.CACHE_KEY_PREFIX_BY_PLATFORM = {
56
- [eas_build_job_1.Platform.ANDROID]: ANDROID_CACHE_KEY_PREFIX,
57
- [eas_build_job_1.Platform.IOS]: IOS_CACHE_KEY_PREFIX,
58
- };
57
+ function getCcacheKeyPrefix(target) {
58
+ if (target.platform === eas_build_job_1.Platform.IOS) {
59
+ return target.simulator ? IOS_SIMULATOR_CACHE_KEY_PREFIX : IOS_DEVICE_CACHE_KEY_PREFIX;
60
+ }
61
+ return ANDROID_CACHE_KEY_PREFIX;
62
+ }
59
63
  exports.PUBLIC_CACHE_KEY_PREFIX_BY_PLATFORM = {
60
64
  [eas_build_job_1.Platform.ANDROID]: PUBLIC_ANDROID_CACHE_KEY_PREFIX,
61
65
  [eas_build_job_1.Platform.IOS]: PUBLIC_IOS_CACHE_KEY_PREFIX,
@@ -68,14 +72,14 @@ function getCcachePath(env) {
68
72
  (0, assert_1.default)(env.HOME, 'Failed to infer directory: $HOME environment variable is empty.');
69
73
  return path_1.default.join(env.HOME, PATH_BY_PLATFORM[os_1.default.platform()]);
70
74
  }
71
- async function generateDefaultBuildCacheKeyAsync(workingDirectory, platform) {
75
+ async function generateDefaultBuildCacheKeyAsync(workingDirectory, target) {
72
76
  // This will resolve which package manager and use the relevant lock file
73
77
  // The lock file hash is the key and ensures cache is fresh
74
78
  const packagerRunDir = (0, packageManager_1.findPackagerRootDir)(workingDirectory);
75
79
  const manager = PackageManagerUtils.createForProject(packagerRunDir);
76
80
  const lockPath = path_1.default.join(packagerRunDir, manager.lockFile);
77
81
  try {
78
- return `${exports.CACHE_KEY_PREFIX_BY_PLATFORM[platform]}${(0, steps_1.hashFiles)([lockPath])}`;
82
+ return `${getCcacheKeyPrefix(target)}${(0, steps_1.hashFiles)([lockPath])}`;
79
83
  }
80
84
  catch (err) {
81
85
  throw new Error(`Failed to read lockfile for cache key generation: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@expo/build-tools",
3
- "version": "22.2.0",
3
+ "version": "22.3.0",
4
4
  "bugs": "https://github.com/expo/eas-cli/issues",
5
5
  "license": "BUSL-1.1",
6
6
  "author": "Expo <support@expo.io>",
@@ -54,6 +54,7 @@
54
54
  "@sentry/node": "7.77.0",
55
55
  "@urql/core": "^6.0.1",
56
56
  "bplist-parser": "0.3.2",
57
+ "content-disposition": "1.0.1",
57
58
  "fast-glob": "^3.3.2",
58
59
  "fast-xml-parser": "^4.4.1",
59
60
  "fs-extra": "^11.2.0",
@@ -77,7 +78,8 @@
77
78
  "zod": "^4.3.5"
78
79
  },
79
80
  "devDependencies": {
80
- "@expo/repack-app": "~0.6.1",
81
+ "@expo/repack-app": "~0.9.0",
82
+ "@types/content-disposition": "0.5.9",
81
83
  "@types/fs-extra": "^11.0.4",
82
84
  "@types/jest": "^29.5.12",
83
85
  "@types/lodash": "^4.17.4",
@@ -100,5 +102,5 @@
100
102
  "typescript": "^5.5.4",
101
103
  "uuid": "^9.0.1"
102
104
  },
103
- "gitHead": "5484dd607468c7711d915a2521df10531f8b9908"
105
+ "gitHead": "70066fc8beedabdd31081f18c18928461d400806"
104
106
  }