From 808383c7498a80f1420b3872f2adbc636cc14aa9 Mon Sep 17 00:00:00 2001 From: "expo-tuft[bot]" <288127324+expo-tuft[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 17:53:45 +0000 Subject: [PATCH 1/2] [build-tools] Regenerate Android autolinking after cache restore Co-authored-by: abbanmustafa <19319135+abbanmustafa@users.noreply.github.com> --- .../src/android/__tests__/autolinking.test.ts | 30 +++++++++++++++++++ .../build-tools/src/android/autolinking.ts | 13 ++++++++ .../src/builders/__tests__/android.test.ts | 27 +++++++++++++++++ packages/build-tools/src/builders/android.ts | 2 ++ 4 files changed, 72 insertions(+) create mode 100644 packages/build-tools/src/android/__tests__/autolinking.test.ts create mode 100644 packages/build-tools/src/android/autolinking.ts diff --git a/packages/build-tools/src/android/__tests__/autolinking.test.ts b/packages/build-tools/src/android/__tests__/autolinking.test.ts new file mode 100644 index 0000000000..1851358750 --- /dev/null +++ b/packages/build-tools/src/android/__tests__/autolinking.test.ts @@ -0,0 +1,30 @@ +import fs from 'fs-extra'; +import path from 'path'; + +import { clearReactNativeAutolinkingCacheAsync } from '../autolinking'; + +describe(clearReactNativeAutolinkingCacheAsync, () => { + it('removes the generated autolinking cache without removing other Android build output', async () => { + const projectDirectory = '/workingdir/build'; + const autolinkingDirectory = path.join(projectDirectory, 'android/build/generated/autolinking'); + const unrelatedBuildOutput = path.join(projectDirectory, 'android/build/outputs/keep.txt'); + + await fs.outputJson(path.join(autolinkingDirectory, 'autolinking.json'), { + dependencies: { + 'react-native-screens': { + platforms: { + android: { + sourceDir: 'C:\\Users\\expo\\app\\node_modules\\react-native-screens\\android', + }, + }, + }, + }, + }); + await fs.outputFile(unrelatedBuildOutput, 'keep'); + + await clearReactNativeAutolinkingCacheAsync(projectDirectory); + + await expect(fs.pathExists(autolinkingDirectory)).resolves.toBe(false); + await expect(fs.readFile(unrelatedBuildOutput, 'utf8')).resolves.toBe('keep'); + }); +}); diff --git a/packages/build-tools/src/android/autolinking.ts b/packages/build-tools/src/android/autolinking.ts new file mode 100644 index 0000000000..db2ab159b7 --- /dev/null +++ b/packages/build-tools/src/android/autolinking.ts @@ -0,0 +1,13 @@ +import fs from 'fs-extra'; +import path from 'path'; + +/** + * React Native's generated autolinking state contains absolute paths. A user cache created on a + * different operating system can therefore link Gradle projects to directories that do not exist + * on the worker. Always regenerate this small cache after restoring user-controlled build output. + */ +export async function clearReactNativeAutolinkingCacheAsync( + projectDirectory: string +): Promise { + await fs.remove(path.join(projectDirectory, 'android/build/generated/autolinking')); +} diff --git a/packages/build-tools/src/builders/__tests__/android.test.ts b/packages/build-tools/src/builders/__tests__/android.test.ts index f62fde94c3..325ea9f7fe 100644 --- a/packages/build-tools/src/builders/__tests__/android.test.ts +++ b/packages/build-tools/src/builders/__tests__/android.test.ts @@ -6,6 +6,7 @@ import { createMockLogger } from '../../__tests__/utils/logger'; import { BuildContext } from '../../context'; import { Datadog } from '../../datadog'; import { restoreCredentials } from '../../android/credentials'; +import { clearReactNativeAutolinkingCacheAsync } from '../../android/autolinking'; import { uploadEmbeddedBundleAsync } from '../../utils/expoUpdatesEmbedded'; import androidBuilder from '../android'; import { runBuilderWithHooksAsync } from '../common'; @@ -25,6 +26,7 @@ jest.mock('../custom', () => ({ runCustomBuildAsync: jest.fn(), })); jest.mock('../../android/credentials'); +jest.mock('../../android/autolinking'); jest.mock('../../android/gradle', () => ({ ensureLFLineEndingsInGradlewScript: jest.fn(), resolveGradleCommand: jest.fn(() => ':app:bundleRelease'), @@ -43,9 +45,11 @@ jest.mock('../../common/setup', () => ({ jest.mock('../../steps/functions/restoreBuildCache', () => ({ cacheStatsAsync: jest.fn(), restoreCcacheAsync: jest.fn(), + restoreGradleCacheAsync: jest.fn(), })); jest.mock('../../steps/functions/saveBuildCache', () => ({ saveCcacheAsync: jest.fn(), + saveGradleCacheAsync: jest.fn(), })); jest.mock('../../steps/utils/android/gradleConfig', () => ({ ...jest.requireActual('../../steps/utils/android/gradleConfig'), @@ -90,6 +94,29 @@ describe(androidBuilder, () => { ); }); + it('clears generated autolinking state after restoring the user cache', async () => { + const restoreCache = jest.fn(); + const ctx = new BuildContext(createTestAndroidJob(), { + workingdir: '/workingdir', + logBuffer: { getLogs: () => [], getPhaseLogs: () => [] }, + logger: createMockLogger(), + env: { + __API_SERVER_URL: 'http://api.expo.test', + EAS_BUILD_RUNNER: 'eas-build', + }, + uploadArtifact: jest.fn(), + cacheManager: { restoreCache, saveCache: jest.fn() }, + }); + + await androidBuilder(ctx); + + expect(restoreCache).toHaveBeenCalled(); + expect(clearReactNativeAutolinkingCacheAsync).toHaveBeenCalledWith('/workingdir/build'); + expect(restoreCache.mock.invocationCallOrder[0]).toBeLessThan( + jest.mocked(clearReactNativeAutolinkingCacheAsync).mock.invocationCallOrder[0] + ); + }); + it('passes the install_node_modules anchor and a hooks ref to setupAsync', async () => { const ctx = new BuildContext(createTestAndroidJob(), { workingdir: '/workingdir', diff --git a/packages/build-tools/src/builders/android.ts b/packages/build-tools/src/builders/android.ts index 0dd2472675..1ec3f341f9 100644 --- a/packages/build-tools/src/builders/android.ts +++ b/packages/build-tools/src/builders/android.ts @@ -5,6 +5,7 @@ import path from 'path'; import { runBuilderWithHooksAsync } from './common'; import { runCustomBuildAsync } from './custom'; import { restoreCredentials } from '../android/credentials'; +import { clearReactNativeAutolinkingCacheAsync } from '../android/autolinking'; import { ensureLFLineEndingsInGradlewScript, resolveGradleCommand, @@ -95,6 +96,7 @@ async function buildInnerAsync( return; } await ctx.cacheManager?.restoreCache(ctx); + await clearReactNativeAutolinkingCacheAsync(workingDirectory); await restoreCcacheAsync({ logger: ctx.logger, workingDirectory, From 6cc7f36c04286e4c9c8442c4cb43c048978c9b99 Mon Sep 17 00:00:00 2001 From: "expo-tuft[bot]" <288127324+expo-tuft[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 20:18:54 +0000 Subject: [PATCH 2/2] [build-tools] Add autolinking cache fix changelog Co-authored-by: abbanmustafa <19319135+abbanmustafa@users.noreply.github.com> --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b50d03410..e74a0cd7ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ This is the log of notable changes to EAS CLI and related packages. ### ๐Ÿ› Bug fixes +- [build-tools] Regenerate Android autolinking metadata after cache restore so paths from another operating system cannot break Gradle project discovery. ([#4288](https://github.com/expo/eas-cli/pull/4288) by [@AbbanMustafa](https://github.com/AbbanMustafa)) + - [build-tools] Preapprove custom URL schemes before opening them in iOS Simulator sessions to avoid the first-use confirmation prompt. ([#4274](https://github.com/expo/eas-cli/pull/4274) by [@szdziedzic](https://github.com/szdziedzic)) ### ๐Ÿงน Chores