@expo/build-tools 18.4.0 → 18.5.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.
Files changed (37) hide show
  1. package/dist/builders/android.js +13 -1
  2. package/dist/builders/ios.js +1 -1
  3. package/dist/common/setup.js +4 -3
  4. package/dist/context.d.ts +1 -1
  5. package/dist/context.js +1 -1
  6. package/dist/steps/easFunctions.js +0 -2
  7. package/dist/steps/functionGroups/maestroTest.js +0 -3
  8. package/dist/steps/functions/calculateEASUpdateRuntimeVersion.js +2 -2
  9. package/dist/steps/functions/configureEASUpdateIfInstalled.js +2 -2
  10. package/dist/steps/functions/repack.js +20 -3
  11. package/dist/steps/functions/restoreBuildCache.d.ts +8 -0
  12. package/dist/steps/functions/restoreBuildCache.js +85 -0
  13. package/dist/steps/functions/saveBuildCache.d.ts +8 -0
  14. package/dist/steps/functions/saveBuildCache.js +57 -0
  15. package/dist/steps/functions/startAndroidEmulator.js +19 -0
  16. package/dist/steps/functions/uploadToAsc.d.ts +3 -0
  17. package/dist/steps/functions/uploadToAsc.js +24 -1
  18. package/dist/steps/utils/ios/AscApiClient.d.ts +1 -1
  19. package/dist/steps/utils/ios/AscApiUtils.d.ts +5 -0
  20. package/dist/steps/utils/ios/AscApiUtils.js +16 -13
  21. package/dist/utils/AndroidEmulatorUtils.d.ts +5 -0
  22. package/dist/utils/AndroidEmulatorUtils.js +17 -0
  23. package/dist/utils/appConfig.d.ts +4 -2
  24. package/dist/utils/appConfig.js +36 -5
  25. package/dist/utils/expoCli.d.ts +4 -0
  26. package/dist/utils/expoCli.js +37 -0
  27. package/dist/utils/expoUpdates.d.ts +1 -1
  28. package/dist/utils/expoUpdates.js +4 -4
  29. package/dist/utils/gradleCacheKey.d.ts +2 -0
  30. package/dist/utils/gradleCacheKey.js +56 -0
  31. package/dist/utils/packageManager.d.ts +10 -0
  32. package/dist/utils/packageManager.js +21 -0
  33. package/package.json +14 -13
  34. package/dist/steps/functions/internalMaestroTest.d.ts +0 -9
  35. package/dist/steps/functions/internalMaestroTest.js +0 -534
  36. package/dist/utils/findMaestroPathsFlowsToExecuteAsync.d.ts +0 -8
  37. package/dist/utils/findMaestroPathsFlowsToExecuteAsync.js +0 -184
@@ -71,6 +71,12 @@ async function buildAsync(ctx) {
71
71
  env: ctx.env,
72
72
  secrets: ctx.job.secrets,
73
73
  });
74
+ await (0, restoreBuildCache_1.restoreGradleCacheAsync)({
75
+ logger: ctx.logger,
76
+ workingDirectory,
77
+ env: ctx.env,
78
+ secrets: ctx.job.secrets,
79
+ });
74
80
  });
75
81
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.POST_INSTALL_HOOK, async () => {
76
82
  await (0, hooks_1.runHookIfPresent)(ctx, hooks_1.Hook.POST_INSTALL);
@@ -79,7 +85,7 @@ async function buildAsync(ctx) {
79
85
  return await (0, expoUpdates_1.resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync)({
80
86
  cwd: ctx.getReactNativeProjectDirectory(),
81
87
  logger: ctx.logger,
82
- appConfig: ctx.appConfig,
88
+ appConfig: await ctx.appConfig,
83
89
  platform: ctx.job.platform,
84
90
  workflow: ctx.job.type,
85
91
  env: ctx.env,
@@ -169,6 +175,12 @@ async function buildAsync(ctx) {
169
175
  env: ctx.env,
170
176
  secrets: ctx.job.secrets,
171
177
  });
178
+ await (0, saveBuildCache_1.saveGradleCacheAsync)({
179
+ logger: ctx.logger,
180
+ workingDirectory,
181
+ env: ctx.env,
182
+ secrets: ctx.job.secrets,
183
+ });
172
184
  });
173
185
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.CACHE_STATS, async () => {
174
186
  await (0, restoreBuildCache_1.cacheStatsAsync)({
@@ -95,7 +95,7 @@ async function buildAsync(ctx) {
95
95
  return await (0, expoUpdates_1.resolveRuntimeVersionForExpoUpdatesIfConfiguredAsync)({
96
96
  cwd: ctx.getReactNativeProjectDirectory(),
97
97
  logger: ctx.logger,
98
- appConfig: ctx.appConfig,
98
+ appConfig: await ctx.appConfig,
99
99
  platform: ctx.job.platform,
100
100
  workflow: ctx.job.type,
101
101
  env: ctx.env,
@@ -90,17 +90,18 @@ async function setupAsync(ctx) {
90
90
  });
91
91
  });
92
92
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.READ_APP_CONFIG, async () => {
93
- const appConfig = ctx.appConfig;
93
+ const appConfig = await ctx.appConfig;
94
94
  ctx.logger.info('Using app configuration:');
95
95
  ctx.logger.info(JSON.stringify(appConfig, null, 2));
96
96
  await validateAppConfigAsync(ctx, appConfig);
97
97
  });
98
98
  if (ctx.job.triggeredBy === eas_build_job_1.BuildTrigger.GIT_BASED_INTEGRATION) {
99
99
  await ctx.runBuildPhase(eas_build_job_1.BuildPhase.EAS_BUILD_INTERNAL, async () => {
100
- if (!ctx.appConfig.ios?.bundleIdentifier && ctx.job.platform === eas_build_job_1.Platform.IOS) {
100
+ const appConfig = await ctx.appConfig;
101
+ if (!appConfig.ios?.bundleIdentifier && ctx.job.platform === eas_build_job_1.Platform.IOS) {
101
102
  throw new Error('The "ios.bundleIdentifier" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#bundleidentifier.');
102
103
  }
103
- if (!ctx.appConfig.android?.package && ctx.job.platform === eas_build_job_1.Platform.ANDROID) {
104
+ if (!appConfig.android?.package && ctx.job.platform === eas_build_job_1.Platform.ANDROID) {
104
105
  throw new Error('The "android.package" is required to be set in app config for builds triggered by GitHub integration. Learn more: https://docs.expo.dev/versions/latest/config/app/#package.');
105
106
  }
106
107
  const { newJob, newMetadata } = await (0, easBuildInternal_1.runEasBuildInternalAsync)({
package/dist/context.d.ts CHANGED
@@ -92,7 +92,7 @@ export declare class BuildContext<TJob extends Job = Job> {
92
92
  */
93
93
  get buildEnvsDirectory(): string;
94
94
  get packageManager(): PackageManager;
95
- get appConfig(): ExpoConfig;
95
+ get appConfig(): Promise<ExpoConfig>;
96
96
  runBuildPhase<T>(buildPhase: BuildPhase, phase: () => Promise<T>, { doNotMarkStart, doNotMarkEnd, }?: {
97
97
  doNotMarkStart?: boolean;
98
98
  doNotMarkEnd?: boolean;
package/dist/context.js CHANGED
@@ -113,7 +113,7 @@ class BuildContext {
113
113
  env: this.env,
114
114
  logger: this.logger,
115
115
  sdkVersion: this.metadata?.sdkVersion,
116
- }).exp;
116
+ }).then(config => config.exp);
117
117
  }
118
118
  return this._appConfig;
119
119
  }
@@ -18,7 +18,6 @@ const injectAndroidCredentials_1 = require("./functions/injectAndroidCredentials
18
18
  const installMaestro_1 = require("./functions/installMaestro");
19
19
  const installNodeModules_1 = require("./functions/installNodeModules");
20
20
  const installPods_1 = require("./functions/installPods");
21
- const internalMaestroTest_1 = require("./functions/internalMaestroTest");
22
21
  const prebuild_1 = require("./functions/prebuild");
23
22
  const readIpaInfo_1 = require("./functions/readIpaInfo");
24
23
  const repack_1 = require("./functions/repack");
@@ -73,7 +72,6 @@ function getEasFunctions(ctx) {
73
72
  (0, calculateEASUpdateRuntimeVersion_1.calculateEASUpdateRuntimeVersionFunction)(),
74
73
  (0, createSubmissionEntity_1.createSubmissionEntityFunction)(),
75
74
  (0, uploadToAsc_1.createUploadToAscBuildFunction)(),
76
- (0, internalMaestroTest_1.createInternalEasMaestroTestFunction)(ctx),
77
75
  (0, reportMaestroTestResults_1.createReportMaestroTestResultsFunction)(ctx),
78
76
  ];
79
77
  if (ctx.hasBuildJob()) {
@@ -39,7 +39,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
39
39
  }) ?? 'ios/build/Build/Products/*simulator/*.app';
40
40
  steps.push(new steps_1.BuildStep(globalCtx, {
41
41
  id: steps_1.BuildStep.getNewId(),
42
- name: 'install_app',
43
42
  displayName: `Install app to Simulator`,
44
43
  command: `
45
44
  # shopt -s nullglob is necessary not to try to install
@@ -78,7 +77,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
78
77
  }) ?? 'android/app/build/outputs/**/*.apk';
79
78
  steps.push(new steps_1.BuildStep(globalCtx, {
80
79
  id: steps_1.BuildStep.getNewId(),
81
- name: 'install_app',
82
80
  displayName: `Install app to Emulator`,
83
81
  command: `
84
82
  # shopt -s globstar is necessary to add /**/ support
@@ -111,7 +109,6 @@ function createEasMaestroTestFunctionGroup(buildToolsContext) {
111
109
  for (const flowPath of flowPaths) {
112
110
  steps.push(new steps_1.BuildStep(globalCtx, {
113
111
  id: steps_1.BuildStep.getNewId(),
114
- name: 'maestro_test',
115
112
  ifCondition: '${ always() }',
116
113
  displayName: `maestro test ${flowPath}`,
117
114
  command: `maestro test ${flowPath}`,
@@ -30,7 +30,7 @@ function calculateEASUpdateRuntimeVersionFunction() {
30
30
  }),
31
31
  ],
32
32
  fn: async (stepCtx, { env, inputs, outputs }) => {
33
- const appConfig = (0, appConfig_1.readAppConfig)({
33
+ const appConfig = (await (0, appConfig_1.readAppConfig)({
34
34
  projectDir: stepCtx.workingDirectory,
35
35
  env: Object.keys(env).reduce((acc, key) => {
36
36
  acc[key] = env[key] ?? '';
@@ -38,7 +38,7 @@ function calculateEASUpdateRuntimeVersionFunction() {
38
38
  }, {}),
39
39
  logger: stepCtx.logger,
40
40
  sdkVersion: stepCtx.global.staticContext.metadata?.sdkVersion,
41
- }).exp;
41
+ })).exp;
42
42
  const platform = inputs.platform.value ?? stepCtx.global.staticContext.job.platform;
43
43
  const workflow = inputs.workflow.value ?? stepCtx.global.staticContext.job.type;
44
44
  if (![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
@@ -44,7 +44,7 @@ function configureEASUpdateIfInstalledFunction() {
44
44
  const job = stepCtx.global.staticContext.job;
45
45
  (0, assert_1.default)(job.platform, 'Configuring EAS Update in generic jobs is not supported.');
46
46
  const metadata = stepCtx.global.staticContext.metadata;
47
- const appConfig = (0, appConfig_1.readAppConfig)({
47
+ const appConfig = (await (0, appConfig_1.readAppConfig)({
48
48
  projectDir: stepCtx.workingDirectory,
49
49
  env: Object.keys(env).reduce((acc, key) => {
50
50
  acc[key] = env[key] ?? '';
@@ -52,7 +52,7 @@ function configureEASUpdateIfInstalledFunction() {
52
52
  }, {}),
53
53
  logger: stepCtx.logger,
54
54
  sdkVersion: metadata?.sdkVersion,
55
- }).exp;
55
+ })).exp;
56
56
  const channelInput = inputs.channel.value;
57
57
  const runtimeVersionInput = inputs.runtime_version.value;
58
58
  const resolvedRuntimeVersionInput = inputs.resolved_eas_update_runtime_version.value;
@@ -15,6 +15,7 @@ const node_path_1 = __importDefault(require("node:path"));
15
15
  const resolve_from_1 = __importDefault(require("resolve-from"));
16
16
  const fastlane_1 = require("../../common/fastlane");
17
17
  const manager_1 = __importDefault(require("../utils/ios/credentials/manager"));
18
+ const packageManager_1 = require("../../utils/packageManager");
18
19
  function createRepackBuildFunction() {
19
20
  return new steps_1.BuildFunction({
20
21
  namespace: 'eas',
@@ -42,6 +43,11 @@ function createRepackBuildFunction() {
42
43
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
43
44
  required: false,
44
45
  }),
46
+ steps_1.BuildStepInput.createProvider({
47
+ id: 'js_bundle_only',
48
+ allowedValueTypeName: steps_1.BuildStepInputValueTypeName.BOOLEAN,
49
+ required: false,
50
+ }),
45
51
  steps_1.BuildStepInput.createProvider({
46
52
  id: 'repack_version',
47
53
  allowedValueTypeName: steps_1.BuildStepInputValueTypeName.STRING,
@@ -68,7 +74,10 @@ function createRepackBuildFunction() {
68
74
  if (![eas_build_job_1.Platform.ANDROID, eas_build_job_1.Platform.IOS].includes(platform)) {
69
75
  throw new Error(`Unsupported platform: ${platform}. Platform must be "${eas_build_job_1.Platform.ANDROID}" or "${eas_build_job_1.Platform.IOS}"`);
70
76
  }
71
- const repackSpawnAsync = createSpawnAsyncStepAdapter({ verbose, logger: stepsCtx.logger });
77
+ const repackSpawnAsync = createSpawnAsyncStepAdapter({
78
+ verbose,
79
+ logger: stepsCtx.logger,
80
+ });
72
81
  const tmpDir = await node_fs_1.default.promises.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), `repack-`));
73
82
  const workingDirectory = node_path_1.default.join(tmpDir, 'working-directory');
74
83
  await node_fs_1.default.promises.mkdir(workingDirectory);
@@ -81,10 +90,16 @@ function createRepackBuildFunction() {
81
90
  sourcemapOutput: undefined,
82
91
  }
83
92
  : undefined;
84
- stepsCtx.logger.info(`Using repack from: ${inputs.repack_package.value}@${inputs.repack_version.value}`);
93
+ const jsBundleOnly = inputs.js_bundle_only.value ?? false;
94
+ const resolvedRepackVersion = (await (0, packageManager_1.resolvePackageVersionAsync)({
95
+ logger: stepsCtx.logger,
96
+ packageName: inputs.repack_package.value,
97
+ distTag: inputs.repack_version.value,
98
+ })) ?? inputs.repack_version.value;
99
+ stepsCtx.logger.info(`Using repack from: ${inputs.repack_package.value}@${inputs.repack_version.value} (${resolvedRepackVersion})`);
85
100
  const repackApp = await installAndImportRepackAsync({
86
101
  packageName: inputs.repack_package.value,
87
- version: inputs.repack_version.value,
102
+ version: resolvedRepackVersion,
88
103
  });
89
104
  const { repackAppIosAsync, repackAppAndroidAsync } = repackApp;
90
105
  stepsCtx.logger.info('Repacking the app...');
@@ -97,6 +112,7 @@ function createRepackBuildFunction() {
97
112
  outputPath,
98
113
  workingDirectory,
99
114
  exportEmbedOptions,
115
+ jsBundleOnly,
100
116
  iosSigningOptions: await resolveIosSigningOptionsAsync({
101
117
  job: stepsCtx.global.staticContext.job,
102
118
  logger: stepsCtx.logger,
@@ -124,6 +140,7 @@ function createRepackBuildFunction() {
124
140
  outputPath,
125
141
  workingDirectory,
126
142
  exportEmbedOptions,
143
+ jsBundleOnly,
127
144
  androidSigningOptions,
128
145
  logger: stepsCtx.logger,
129
146
  spawnAsync: repackSpawnAsync,
@@ -12,6 +12,14 @@ export declare function restoreCcacheAsync({ logger, workingDirectory, platform,
12
12
  robotAccessToken?: string;
13
13
  };
14
14
  }): Promise<void>;
15
+ export declare function restoreGradleCacheAsync({ 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 cacheStatsAsync({ logger, env, secrets, }: {
16
24
  logger: bunyan;
17
25
  env: Record<string, string | undefined>;
@@ -6,14 +6,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createRestoreBuildCacheFunction = createRestoreBuildCacheFunction;
7
7
  exports.createCacheStatsBuildFunction = createCacheStatsBuildFunction;
8
8
  exports.restoreCcacheAsync = restoreCcacheAsync;
9
+ exports.restoreGradleCacheAsync = restoreGradleCacheAsync;
9
10
  exports.cacheStatsAsync = cacheStatsAsync;
10
11
  const eas_build_job_1 = require("@expo/eas-build-job");
11
12
  const results_1 = require("@expo/results");
12
13
  const steps_1 = require("@expo/steps");
14
+ const fs_1 = __importDefault(require("fs"));
13
15
  const nullthrows_1 = __importDefault(require("nullthrows"));
16
+ const os_1 = __importDefault(require("os"));
17
+ const path_1 = __importDefault(require("path"));
14
18
  const ccacheStats_1 = require("./ccacheStats");
15
19
  const restoreCache_1 = require("./restoreCache");
16
20
  const cacheKey_1 = require("../../utils/cacheKey");
21
+ const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
17
22
  const turtleFetch_1 = require("../../utils/turtleFetch");
18
23
  function createRestoreBuildCacheFunction() {
19
24
  return new steps_1.BuildFunction({
@@ -43,6 +48,14 @@ function createRestoreBuildCacheFunction() {
43
48
  env,
44
49
  secrets: stepCtx.global.staticContext.job.secrets,
45
50
  });
51
+ if (platform === eas_build_job_1.Platform.ANDROID) {
52
+ await restoreGradleCacheAsync({
53
+ logger,
54
+ workingDirectory,
55
+ env,
56
+ secrets: stepCtx.global.staticContext.job.secrets,
57
+ });
58
+ }
46
59
  },
47
60
  });
48
61
  }
@@ -140,6 +153,78 @@ async function restoreCcacheAsync({ logger, workingDirectory, platform, env, sec
140
153
  }
141
154
  }
142
155
  }
156
+ async function restoreGradleCacheAsync({ logger, workingDirectory, env, secrets, }) {
157
+ if (env.EXPERIMENTAL_EAS_GRADLE_CACHE !== '1') {
158
+ return;
159
+ }
160
+ try {
161
+ const gradlePropertiesPath = path_1.default.join(workingDirectory, 'android', 'gradle.properties');
162
+ const gradlePropertiesContent = await fs_1.default.promises.readFile(gradlePropertiesPath, 'utf-8');
163
+ await fs_1.default.promises.writeFile(gradlePropertiesPath, `${gradlePropertiesContent}\n\norg.gradle.caching=true\n`);
164
+ // Configure cache cleanup via init script (works with both Gradle 8 and 9,
165
+ // org.gradle.cache.cleanup property was removed in Gradle 9)
166
+ const initScriptDir = path_1.default.join(os_1.default.homedir(), '.gradle', 'init.d');
167
+ await fs_1.default.promises.mkdir(initScriptDir, { recursive: true });
168
+ await fs_1.default.promises.writeFile(path_1.default.join(initScriptDir, 'eas-cache-cleanup.gradle'), [
169
+ 'def cacheDir = new File(System.getProperty("user.home"), ".gradle/caches/build-cache-1")',
170
+ 'def countBefore = cacheDir.exists() ? cacheDir.listFiles()?.length ?: 0 : 0',
171
+ 'println "[EAS] Gradle build cache entries before cleanup: ${countBefore}"',
172
+ '',
173
+ 'beforeSettings { settings ->',
174
+ ' try {',
175
+ ' settings.caches {',
176
+ ' cleanup = Cleanup.ALWAYS',
177
+ ' buildCache {',
178
+ ' setRemoveUnusedEntriesAfterDays(3)',
179
+ ' }',
180
+ ' }',
181
+ ' println "[EAS] Configured Gradle cache cleanup via init script"',
182
+ ' } catch (Exception e) {',
183
+ ' println "[EAS] Failed to configure cache cleanup: ${e.message}"',
184
+ ' }',
185
+ '}',
186
+ '',
187
+ 'gradle.buildFinished {',
188
+ ' def countAfter = cacheDir.exists() ? cacheDir.listFiles()?.length ?: 0 : 0',
189
+ ' println "[EAS] Gradle build cache entries after build: ${countAfter} (was ${countBefore})"',
190
+ '}',
191
+ '',
192
+ ].join('\n'));
193
+ const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
194
+ const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
195
+ const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
196
+ const cacheKey = await (0, gradleCacheKey_1.generateGradleCacheKeyAsync)(workingDirectory);
197
+ logger.info(`Restoring Gradle cache key: ${cacheKey}`);
198
+ const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
199
+ const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
200
+ const { archivePath, matchedKey } = await (0, restoreCache_1.downloadCacheAsync)({
201
+ logger,
202
+ jobId,
203
+ expoApiServerURL,
204
+ robotAccessToken,
205
+ paths: [buildCachePath],
206
+ key: cacheKey,
207
+ keyPrefixes: [gradleCacheKey_1.GRADLE_CACHE_KEY_PREFIX],
208
+ platform: eas_build_job_1.Platform.ANDROID,
209
+ });
210
+ await fs_1.default.promises.mkdir(gradleCachesPath, { recursive: true });
211
+ await (0, restoreCache_1.decompressCacheAsync)({
212
+ archivePath,
213
+ workingDirectory: gradleCachesPath,
214
+ verbose: env.EXPO_DEBUG === '1',
215
+ logger,
216
+ });
217
+ logger.info(`Gradle cache restored to ${gradleCachesPath} ${matchedKey === cacheKey ? '(direct hit)' : '(prefix match)'}`);
218
+ }
219
+ catch (err) {
220
+ if (err instanceof turtleFetch_1.TurtleFetchError && err.response?.status === 404) {
221
+ logger.info('No Gradle cache found for this key');
222
+ }
223
+ else {
224
+ logger.warn('Failed to restore Gradle cache: ', err);
225
+ }
226
+ }
227
+ }
143
228
  async function cacheStatsAsync({ logger, env, secrets, }) {
144
229
  const enabled = env.EAS_RESTORE_CACHE === '1' || (env.EAS_USE_CACHE === '1' && env.EAS_RESTORE_CACHE !== '0');
145
230
  if (!enabled) {
@@ -12,3 +12,11 @@ export declare function saveCcacheAsync({ logger, workingDirectory, platform, ev
12
12
  robotAccessToken?: string;
13
13
  };
14
14
  }): Promise<void>;
15
+ export declare function saveGradleCacheAsync({ 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>;
@@ -5,13 +5,18 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.createSaveBuildCacheFunction = createSaveBuildCacheFunction;
7
7
  exports.saveCcacheAsync = saveCcacheAsync;
8
+ exports.saveGradleCacheAsync = saveGradleCacheAsync;
8
9
  const eas_build_job_1 = require("@expo/eas-build-job");
9
10
  const results_1 = require("@expo/results");
10
11
  const steps_1 = require("@expo/steps");
11
12
  const fs_1 = __importDefault(require("fs"));
12
13
  const nullthrows_1 = __importDefault(require("nullthrows"));
14
+ const os_1 = __importDefault(require("os"));
15
+ const path_1 = __importDefault(require("path"));
13
16
  const saveCache_1 = require("./saveCache");
17
+ const artifacts_1 = require("../../utils/artifacts");
14
18
  const cacheKey_1 = require("../../utils/cacheKey");
19
+ const gradleCacheKey_1 = require("../../utils/gradleCacheKey");
15
20
  function createSaveBuildCacheFunction(evictUsedBefore) {
16
21
  return new steps_1.BuildFunction({
17
22
  namespace: 'eas',
@@ -41,6 +46,14 @@ function createSaveBuildCacheFunction(evictUsedBefore) {
41
46
  env,
42
47
  secrets: stepCtx.global.staticContext.job.secrets,
43
48
  });
49
+ if (platform === eas_build_job_1.Platform.ANDROID) {
50
+ await saveGradleCacheAsync({
51
+ logger,
52
+ workingDirectory,
53
+ env,
54
+ secrets: stepCtx.global.staticContext.job.secrets,
55
+ });
56
+ }
44
57
  },
45
58
  });
46
59
  }
@@ -98,3 +111,47 @@ async function saveCcacheAsync({ logger, workingDirectory, platform, evictUsedBe
98
111
  logger.error({ err }, 'Failed to save cache');
99
112
  }
100
113
  }
114
+ async function saveGradleCacheAsync({ logger, workingDirectory, env, secrets, }) {
115
+ if (env.EXPERIMENTAL_EAS_GRADLE_CACHE !== '1') {
116
+ return;
117
+ }
118
+ const gradleCachesPath = path_1.default.join(os_1.default.homedir(), '.gradle', 'caches');
119
+ const buildCachePath = path_1.default.join(gradleCachesPath, 'build-cache-1');
120
+ try {
121
+ await fs_1.default.promises.access(buildCachePath);
122
+ }
123
+ catch {
124
+ logger.warn('No Gradle build cache found, skipping save');
125
+ return;
126
+ }
127
+ try {
128
+ const cacheKey = await (0, gradleCacheKey_1.generateGradleCacheKeyAsync)(workingDirectory);
129
+ logger.info(`Saving Gradle cache key: ${cacheKey}`);
130
+ const jobId = (0, nullthrows_1.default)(env.EAS_BUILD_ID, 'EAS_BUILD_ID is not set');
131
+ const robotAccessToken = (0, nullthrows_1.default)(secrets?.robotAccessToken, 'Robot access token is required for cache operations');
132
+ const expoApiServerURL = (0, nullthrows_1.default)(env.__API_SERVER_URL, '__API_SERVER_URL is not set');
133
+ logger.info('Compressing Gradle build cache...');
134
+ const { archivePath } = await (0, saveCache_1.compressCacheAsync)({
135
+ paths: [buildCachePath],
136
+ workingDirectory: gradleCachesPath,
137
+ verbose: env.EXPO_DEBUG === '1',
138
+ logger,
139
+ });
140
+ const { size } = await fs_1.default.promises.stat(archivePath);
141
+ logger.info(`Gradle cache archive size: ${(0, artifacts_1.formatBytes)(size)}`);
142
+ await (0, saveCache_1.uploadCacheAsync)({
143
+ logger,
144
+ jobId,
145
+ expoApiServerURL,
146
+ robotAccessToken,
147
+ archivePath,
148
+ key: cacheKey,
149
+ paths: [buildCachePath],
150
+ size,
151
+ platform: eas_build_job_1.Platform.ANDROID,
152
+ });
153
+ }
154
+ catch (err) {
155
+ logger.error({ err }, 'Failed to save Gradle cache');
156
+ }
157
+ }
@@ -57,6 +57,11 @@ function createStartAndroidEmulatorBuildFunction() {
57
57
  const systemImagePackage = `${inputs.system_image_package.value}`;
58
58
  // We can cast because allowedValueTypeName validated this is a string.
59
59
  const deviceIdentifier = inputs.device_identifier.value;
60
+ const shouldAdjustAnimationScale = env.ANDROID_EMULATOR_ADJUST_ANIMATION_SCALE !== 'false' &&
61
+ env.ANDROID_EMULATOR_ADJUST_ANIMATION_SCALE !== '0';
62
+ if (!shouldAdjustAnimationScale) {
63
+ logger.info('Skipping Android emulator animation scale adjustments because $ANDROID_EMULATOR_ADJUST_ANIMATION_SCALE is disabled.');
64
+ }
60
65
  logger.info('Making sure system image is installed');
61
66
  await (0, retry_1.retryAsync)(async () => {
62
67
  await (0, turtle_spawn_1.default)('sdkmanager', [systemImagePackage], {
@@ -99,6 +104,13 @@ function createStartAndroidEmulatorBuildFunction() {
99
104
  timeoutMs,
100
105
  logger,
101
106
  });
107
+ if (shouldAdjustAnimationScale) {
108
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.disableWindowAndTransitionAnimationsAsync({
109
+ env,
110
+ logger,
111
+ serialId: attemptSerialId,
112
+ });
113
+ }
102
114
  logger.info(`${deviceName} is ready.`);
103
115
  serialId = attemptSerialId;
104
116
  emulatorPromise = startResult.emulatorPromise;
@@ -174,6 +186,13 @@ function createStartAndroidEmulatorBuildFunction() {
174
186
  timeoutMs,
175
187
  logger,
176
188
  });
189
+ if (shouldAdjustAnimationScale) {
190
+ await AndroidEmulatorUtils_1.AndroidEmulatorUtils.disableWindowAndTransitionAnimationsAsync({
191
+ env,
192
+ logger,
193
+ serialId: cloneSerialId,
194
+ });
195
+ }
177
196
  logger.info(`${cloneIdentifier} is ready.`);
178
197
  }
179
198
  catch (err) {
@@ -3,6 +3,9 @@ export declare function createUploadToAscBuildFunction(): BuildFunction;
3
3
  export declare function isClosedVersionTrainError(messages: {
4
4
  code: string;
5
5
  }[]): boolean;
6
+ export declare function isSdkVersionIssueError(messages: {
7
+ code: string;
8
+ }[]): boolean;
6
9
  export declare function isInvalidBundleIdentifierError(messages: {
7
10
  code: string;
8
11
  }[]): boolean;
@@ -38,6 +38,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
38
38
  Object.defineProperty(exports, "__esModule", { value: true });
39
39
  exports.createUploadToAscBuildFunction = createUploadToAscBuildFunction;
40
40
  exports.isClosedVersionTrainError = isClosedVersionTrainError;
41
+ exports.isSdkVersionIssueError = isSdkVersionIssueError;
41
42
  exports.isInvalidBundleIdentifierError = isInvalidBundleIdentifierError;
42
43
  exports.isMissingPurposeStringError = isMissingPurposeStringError;
43
44
  exports.parseMissingUsageDescriptionKeys = parseMissingUsageDescriptionKeys;
@@ -247,17 +248,36 @@ function createUploadToAscBuildFunction() {
247
248
  stepsCtx.logger.error(`Errors:\n${itemizeMessages(errors)}\n`);
248
249
  }
249
250
  if (state.state === 'FAILED') {
251
+ if (isSdkVersionIssueError(errors)) {
252
+ throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_SDK_VERSION_ISSUE', 'Build upload was rejected by App Store Connect because the IPA was built with an iOS SDK that is too old for current App Store Connect requirements. ' +
253
+ 'In an Expo project, this usually means the build used an outdated EAS iOS image or the project still depends on an Expo SDK / native setup that does not support the required Xcode toolchain. ' +
254
+ 'Upgrade to a supported Expo SDK if needed and/or select a newer iOS image in `eas.json`, rebuild, and submit again.', {
255
+ docsUrl: 'https://docs.expo.dev/workflow/upgrading-expo-sdk-walkthrough/',
256
+ });
257
+ }
250
258
  if (isInvalidBundleIdentifierError(errors)) {
251
259
  const ipaInfoResult = await (0, results_1.asyncResult)((0, readIpaInfo_1.readIpaInfoAsync)(ipaPath));
252
260
  const ipaBundleIdentifier = ipaInfoResult.ok
253
261
  ? ipaInfoResult.value.bundleIdentifier
254
262
  : null;
263
+ let visibleAppsSummary = null;
264
+ try {
265
+ const apps = await AscApiUtils_1.AscApiUtils.getAppsAsync({ client, limit: 5 });
266
+ visibleAppsSummary = AscApiUtils_1.AscApiUtils.formatAppsList(apps);
267
+ }
268
+ catch {
269
+ // Ok to fail, this is just trying to be helpful.
270
+ }
255
271
  throw new eas_build_job_1.UserError('EAS_UPLOAD_TO_ASC_INVALID_BUNDLE_ID', `Build upload was rejected by App Store Connect because the app bundle identifier in the IPA does not match the selected App Store Connect app.\n\n` +
256
272
  `IPA bundle identifier: ${ipaBundleIdentifier ?? '(unavailable)'}\n` +
257
273
  `App Store Connect app bundle identifier: ${ascAppBundleIdentifier}\n\n` +
258
274
  'Bundle identifier cannot be changed for an existing App Store Connect app. ' +
259
275
  'If you selected the wrong app, change the Apple app identifier in the submit profile. ' +
260
- 'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).');
276
+ 'If you selected the right app, you may want to select a different build to upload (or rebuild with a different profile).' +
277
+ '\n\nOther App Store Connect apps visible to this API key:\n' +
278
+ (visibleAppsSummary ?? ' (unable to retrieve)'), {
279
+ docsUrl: 'https://expo.fyi/asc-app-id',
280
+ });
261
281
  }
262
282
  if (isMissingPurposeStringError(errors)) {
263
283
  const missingUsageDescriptionKeys = parseMissingUsageDescriptionKeys(errors);
@@ -293,6 +313,9 @@ function isClosedVersionTrainError(messages) {
293
313
  return (messages.length > 0 &&
294
314
  messages.every(message => ['90062', '90186', '90478'].includes(message.code)));
295
315
  }
316
+ function isSdkVersionIssueError(messages) {
317
+ return messages.length > 0 && messages.every(message => message.code === '90725');
318
+ }
296
319
  function isInvalidBundleIdentifierError(messages) {
297
320
  return (messages.length > 0 && messages.every(message => ['90054', '90055'].includes(message.code)));
298
321
  }
@@ -92,8 +92,8 @@ declare const GetApi: {
92
92
  }, z.core.$strip>;
93
93
  request: z.ZodObject<{
94
94
  'fields[buildUploads]': z.ZodArray<z.ZodEnum<{
95
- build: "build";
96
95
  state: "state";
96
+ build: "build";
97
97
  }>>;
98
98
  include: z.ZodArray<z.ZodEnum<{
99
99
  build: "build";
@@ -10,4 +10,9 @@ export declare namespace AscApiUtils {
10
10
  bundleShortVersion: string;
11
11
  bundleVersion: string;
12
12
  }): Promise<AscApiClientPostApi['/v1/buildUploads']['response']>;
13
+ function getAppsAsync({ client, limit, }: {
14
+ client: Pick<AscApiClient, 'getAsync'>;
15
+ limit?: number;
16
+ }): Promise<AscApiClientGetApi['/v1/apps']['response']['data']>;
17
+ function formatAppsList(apps: AscApiClientGetApi['/v1/apps']['response']['data']): string;
13
18
  }
@@ -19,7 +19,8 @@ var AscApiUtils;
19
19
  }
20
20
  let visibleAppsSummary = null;
21
21
  try {
22
- visibleAppsSummary = await getVisibleAppsSummaryAsync(client);
22
+ const apps = await AscApiUtils.getAppsAsync({ client, limit: 10 });
23
+ visibleAppsSummary = AscApiUtils.formatAppsList(apps);
23
24
  }
24
25
  catch {
25
26
  // Don't hide the original NOT_FOUND error with a secondary lookup failure.
@@ -75,16 +76,18 @@ var AscApiUtils;
75
76
  }
76
77
  }
77
78
  AscApiUtils.createBuildUploadAsync = createBuildUploadAsync;
78
- })(AscApiUtils || (exports.AscApiUtils = AscApiUtils = {}));
79
- async function getVisibleAppsSummaryAsync(client) {
80
- const appsResponse = await client.getAsync('/v1/apps', {
81
- 'fields[apps]': ['bundleId', 'name'],
82
- limit: 10,
83
- });
84
- if (appsResponse.data.length === 0) {
85
- return ' (none)';
79
+ async function getAppsAsync({ client, limit, }) {
80
+ const appsResponse = await client.getAsync('/v1/apps', {
81
+ 'fields[apps]': ['bundleId', 'name'],
82
+ limit: limit ?? 10,
83
+ });
84
+ return appsResponse.data;
85
+ }
86
+ AscApiUtils.getAppsAsync = getAppsAsync;
87
+ function formatAppsList(apps) {
88
+ return (apps
89
+ .map(app => `- ${app.attributes.name} (${app.attributes.bundleId}) (ID: ${app.id})`)
90
+ .join('\n') || ' (none)');
86
91
  }
87
- return appsResponse.data
88
- .map(app => `- ${app.attributes.name} (${app.attributes.bundleId}) (ID: ${app.id})`)
89
- .join('\n');
90
- }
92
+ AscApiUtils.formatAppsList = formatAppsList;
93
+ })(AscApiUtils || (exports.AscApiUtils = AscApiUtils = {}));
@@ -47,6 +47,11 @@ export declare namespace AndroidEmulatorUtils {
47
47
  timeoutMs?: number;
48
48
  logger?: bunyan;
49
49
  }): Promise<void>;
50
+ function disableWindowAndTransitionAnimationsAsync({ serialId, env, logger, }: {
51
+ serialId: AndroidDeviceSerialId;
52
+ env: NodeJS.ProcessEnv;
53
+ logger: bunyan;
54
+ }): Promise<void>;
50
55
  function collectLogsAsync({ serialId, env, }: {
51
56
  serialId: AndroidDeviceSerialId;
52
57
  env: NodeJS.ProcessEnv;