diff --git a/.changeset/appium-tcc-fda.md b/.changeset/appium-tcc-fda.md new file mode 100644 index 0000000000..b797857597 --- /dev/null +++ b/.changeset/appium-tcc-fda.md @@ -0,0 +1,5 @@ +--- +'@expo/build-tools': patch +--- + +Run Appium through the prepared Full Disk Access helper when available on macOS. diff --git a/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts b/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts index 3c70590799..e53ff0d87b 100644 --- a/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts +++ b/packages/build-tools/src/steps/functions/__tests__/startAppiumRemoteSession.test.ts @@ -1,6 +1,7 @@ import { BuildRuntimePlatform, type BuildStepContext } from '@expo/steps'; import spawn from '@expo/turtle-spawn'; import fs from 'node:fs'; +import path from 'node:path'; import { createGlobalContextMock } from '../../../__tests__/utils/context'; import { type CustomBuildContext } from '../../../customBuildContext'; @@ -202,6 +203,76 @@ describe(installAppiumAsync, () => { }); describe('createStartAppiumRemoteSessionBuildFunction', () => { + it.each([ + [BuildRuntimePlatform.DARWIN, true], + [BuildRuntimePlatform.DARWIN, false], + [BuildRuntimePlatform.LINUX, true], + [BuildRuntimePlatform.LINUX, false], + ])('starts Appium on %s with helper present=%s', async (runtimePlatform, helperPresent) => { + const helper = '/usr/local/libexec/turtle-tcc-fda'; + if (helperPresent) { + fs.mkdirSync(path.dirname(helper), { recursive: true }); + fs.writeFileSync(helper, 'prepared helper'); + } + jest.mocked(spawn).mockResolvedValue({ stdout: '{}' } as never); + jest + .mocked(IosSimulatorUtils.getAvailableDevicesAsync) + .mockResolvedValue([{ udid: 'simulator' }] as never); + jest + .mocked(AndroidEmulatorUtils.getAttachedDevicesAsync) + .mockResolvedValue([{ serialId: 'emulator-5554', state: 'device' }] as never); + // Stop at the launch boundary; no readiness requests or remote session are needed. + const launched = new Error('launch captured'); + jest.mocked(spawnDetached).mockImplementation(() => { + throw launched; + }); + const env = { + DEVICE_RUN_SESSION_ID: 'session', + EAS_SIMULATOR_NGROK_TUNNEL_DOMAIN: 'example.test', + PATH: '/configured/node/bin:/usr/bin', + DEVELOPER_DIR: '/Applications/Xcode.app/Contents/Developer', + }; + const buildFunction = createStartAppiumRemoteSessionBuildFunction({} as CustomBuildContext); + await expect( + buildFunction.fn!( + { logger, global: { runtimePlatform } } as unknown as BuildStepContext, + { + inputs: { + package_version: { value: undefined }, + max_idle_time_minutes: { value: undefined }, + }, + outputs: {}, + env: { ...env, NGROK_AUTHTOKEN: 'token' }, + } as never + ) + ).rejects.toBe(launched); + + expect(spawnDetached).toHaveBeenCalledTimes(1); + const launch = jest.mocked(spawnDetached).mock.calls[0][0]; + const appiumBinPath = path.join(launch.env.APPIUM_HOME!, 'node_modules', '.bin', 'appium'); + const useHelper = runtimePlatform === BuildRuntimePlatform.DARWIN && helperPresent; + expect(launch).toEqual({ + command: useHelper ? helper : appiumBinPath, + args: [ + ...(useHelper ? [appiumBinPath] : []), + '--address', + '127.0.0.1', + '--port', + '4723', + '--base-path', + '/', + '--log-level', + 'error', + '--allow-insecure', + '*:session_discovery', + '--default-capabilities', + '{"appium:eventTimings":true}', + ], + env: { ...env, APPIUM_HOME: launch.env.APPIUM_HOME }, + logger, + }); + }); + it('declares the launch inputs so serve-sim can launch the app', () => { const ctx = {} as unknown as CustomBuildContext; const buildFunction = createStartAppiumRemoteSessionBuildFunction(ctx); diff --git a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts index 4eb0073ad9..57c7d8316c 100644 --- a/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts +++ b/packages/build-tools/src/steps/functions/startAppiumRemoteSession.ts @@ -65,6 +65,7 @@ const APPIUM_HOST = '127.0.0.1'; const APPIUM_PORT = 4723; const APPIUM_STARTUP_TIMEOUT_MS = 120_000; const DEFAULT_APPIUM_VERSION = '^3'; +const FDA_HELPER = '/usr/local/libexec/turtle-tcc-fda'; const AppiumInstalledDriversSchema = z.record( z.string(), @@ -225,9 +226,12 @@ export function createStartAppiumRemoteSessionBuildFunction( }); setupSignal?.throwIfAborted(); + const useFdaHelper = + runtimePlatform === BuildRuntimePlatform.DARWIN && fs.existsSync(FDA_HELPER); appiumProcess = spawnDetached({ - command: appiumBinPath, + command: useFdaHelper ? FDA_HELPER : appiumBinPath, args: [ + ...(useFdaHelper ? [appiumBinPath] : []), '--address', APPIUM_HOST, '--port',