mirror of
https://github.com/expo/eas-cli.git
synced 2026-11-14 10:08:06 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195807643f | ||
|
|
21256e6d55 | ||
|
|
705fa875d3 | ||
|
|
498384357e | ||
|
|
53938e4cf8 | ||
|
|
e18ffcd48e | ||
|
|
df12f86da9 | ||
|
|
3dba7f2bf7 |
No files matched your search
+18
-2
@@ -6,6 +6,24 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
### 🐛 Bug fixes
|
||||
|
||||
- [eas-cli] Support Xcode 27, which replaces Simulator.app with Device Hub, in `build:dev`, `build:run`, and `run` for iOS simulator builds. ([#4405](https://github.com/expo/eas-cli/pull/4405) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [24.4.1](https://github.com/expo/eas-cli/releases/tag/v24.4.1) - 2026-09-15
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
- [eas-cli] Bump `@expo/apple-utils` to `2.2.1`. ([#4404](https://github.com/expo/eas-cli/pull/4404) by [@brentvatne](https://github.com/brentvatne))
|
||||
|
||||
## [24.4.0](https://github.com/expo/eas-cli/releases/tag/v24.4.0) - 2026-09-14
|
||||
|
||||
### 🛠 Breaking changes
|
||||
|
||||
- [eas-cli] Require Node `^20.18.3 || >=22.0.0`. ([#4279](https://github.com/expo/eas-cli/pull/4279) by [@ramonclaudio](https://github.com/ramonclaudio))
|
||||
|
||||
### 🎉 New features
|
||||
@@ -17,8 +35,6 @@ This is the log of notable changes to EAS CLI and related packages.
|
||||
|
||||
- [build-tools] Skip project source refresh for jobs with no sources. ([#4389](https://github.com/expo/eas-cli/pull/4389) by [@sjchmiela](https://github.com/sjchmiela))
|
||||
|
||||
### 🧹 Chores
|
||||
|
||||
## [24.3.0](https://github.com/expo/eas-cli/releases/tag/v24.3.0) - 2026-09-11
|
||||
|
||||
### 🎉 New features
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"STAGING": "24.3.0",
|
||||
"PRODUCTION": "24.3.0"
|
||||
"STAGING": "24.4.1",
|
||||
"PRODUCTION": "24.4.1"
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"npmClient": "yarn",
|
||||
"version": "24.4.0",
|
||||
"version": "24.4.2",
|
||||
"$schema": "node_modules/lerna/schemas/lerna-schema.json"
|
||||
}
|
||||
+133
-133
File diff suppressed because it is too large.
Load diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "eas-cli",
|
||||
"version": "24.4.0",
|
||||
"version": "24.4.2",
|
||||
"description": "EAS command line tool",
|
||||
"keywords": [
|
||||
"cli",
|
||||
@@ -44,7 +44,7 @@
|
||||
"copy-new-templates": "rimraf build/commandUtils/new/templates && mkdir -p build/commandUtils/new && cp -r src/commandUtils/new/templates build/commandUtils/new"
|
||||
},
|
||||
"dependencies": {
|
||||
"@expo/apple-utils": "2.2.0",
|
||||
"@expo/apple-utils": "2.2.1",
|
||||
"@expo/code-signing-certificates": "0.0.5",
|
||||
"@expo/config": "55.0.10",
|
||||
"@expo/config-plugins": "55.0.7",
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
import * as osascript from '@expo/osascript';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
|
||||
import {
|
||||
ensureSimulatorAppOpenedAsync,
|
||||
getSimulatorAppIdAsync,
|
||||
openSimulatorAppAsync,
|
||||
} from '../simulator';
|
||||
|
||||
jest.mock('@expo/osascript');
|
||||
jest.mock('@expo/spawn-async');
|
||||
jest.mock('../../../log');
|
||||
|
||||
const UDID = '5B3F6A41-9F2A-4C7B-8B26-2A2E39B3A1D0';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.mocked(osascript.safeIdOfAppAsync).mockReset();
|
||||
jest.mocked(osascript.execAsync).mockReset();
|
||||
jest.mocked(spawnAsync).mockReset();
|
||||
});
|
||||
|
||||
describe(getSimulatorAppIdAsync, () => {
|
||||
it('returns the Simulator.app id on Xcode 26 and older', async () => {
|
||||
jest.mocked(osascript.safeIdOfAppAsync).mockResolvedValueOnce('com.apple.iphonesimulator');
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBe('com.apple.iphonesimulator');
|
||||
|
||||
expect(osascript.safeIdOfAppAsync).toHaveBeenCalledTimes(1);
|
||||
expect(osascript.safeIdOfAppAsync).toHaveBeenCalledWith('Simulator');
|
||||
expect(spawnAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns the DeviceHub.app id on Xcode 27', async () => {
|
||||
jest
|
||||
.mocked(osascript.safeIdOfAppAsync)
|
||||
.mockResolvedValueOnce(null)
|
||||
.mockResolvedValueOnce('com.apple.dt.Devices');
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBe('com.apple.dt.Devices');
|
||||
|
||||
expect(osascript.safeIdOfAppAsync).toHaveBeenNthCalledWith(1, 'Simulator');
|
||||
expect(osascript.safeIdOfAppAsync).toHaveBeenNthCalledWith(2, 'DeviceHub');
|
||||
expect(spawnAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('still checks DeviceHub when the Simulator lookup returns an empty id', async () => {
|
||||
jest
|
||||
.mocked(osascript.safeIdOfAppAsync)
|
||||
.mockResolvedValueOnce('')
|
||||
.mockResolvedValueOnce('com.apple.dt.Devices');
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBe('com.apple.dt.Devices');
|
||||
});
|
||||
|
||||
it('falls back to the Simulator.app Info.plist inside the selected Xcode', async () => {
|
||||
jest.mocked(osascript.safeIdOfAppAsync).mockResolvedValue(null);
|
||||
jest
|
||||
.mocked(spawnAsync)
|
||||
.mockResolvedValueOnce({ stdout: '/Applications/Xcode.app/Contents/Developer\n' } as any)
|
||||
.mockResolvedValueOnce({ stdout: 'com.apple.iphonesimulator\n' } as any);
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBe('com.apple.iphonesimulator');
|
||||
|
||||
expect(spawnAsync).toHaveBeenNthCalledWith(1, 'xcode-select', ['--print-path']);
|
||||
expect(spawnAsync).toHaveBeenNthCalledWith(2, 'defaults', [
|
||||
'read',
|
||||
'/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/Info.plist',
|
||||
'CFBundleIdentifier',
|
||||
]);
|
||||
});
|
||||
|
||||
it('falls back to the DeviceHub.app Info.plist inside the selected Xcode', async () => {
|
||||
jest.mocked(osascript.safeIdOfAppAsync).mockResolvedValue(null);
|
||||
jest
|
||||
.mocked(spawnAsync)
|
||||
.mockResolvedValueOnce({ stdout: '/Applications/Xcode.app/Contents/Developer\n' } as any)
|
||||
.mockRejectedValueOnce(new Error('does not exist'))
|
||||
.mockResolvedValueOnce({ stdout: 'com.apple.dt.Devices\n' } as any);
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBe('com.apple.dt.Devices');
|
||||
|
||||
expect(spawnAsync).toHaveBeenNthCalledWith(3, 'defaults', [
|
||||
'read',
|
||||
'/Applications/Xcode.app/Contents/Applications/DeviceHub.app/Contents/Info.plist',
|
||||
'CFBundleIdentifier',
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns undefined when neither app can be found', async () => {
|
||||
jest.mocked(osascript.safeIdOfAppAsync).mockResolvedValue(null);
|
||||
jest.mocked(spawnAsync).mockRejectedValue(new Error('not found'));
|
||||
|
||||
await expect(getSimulatorAppIdAsync()).resolves.toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe(openSimulatorAppAsync, () => {
|
||||
it('opens Simulator.app focused on the device on Xcode 26 and older', async () => {
|
||||
jest.mocked(spawnAsync).mockResolvedValue({} as any);
|
||||
|
||||
await openSimulatorAppAsync(UDID);
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledTimes(1);
|
||||
expect(spawnAsync).toHaveBeenCalledWith('open', [
|
||||
'-a',
|
||||
'Simulator',
|
||||
'--args',
|
||||
'-CurrentDeviceUDID',
|
||||
UDID,
|
||||
]);
|
||||
});
|
||||
|
||||
it('opens Device Hub focused on the device when Simulator.app is missing', async () => {
|
||||
jest
|
||||
.mocked(spawnAsync)
|
||||
.mockRejectedValueOnce(new Error('Unable to find application named Simulator'))
|
||||
.mockResolvedValueOnce({} as any);
|
||||
|
||||
await openSimulatorAppAsync(UDID);
|
||||
|
||||
expect(spawnAsync).toHaveBeenNthCalledWith(2, 'open', [`devices://device/open?id=${UDID}`]);
|
||||
});
|
||||
|
||||
it('opens Device Hub without focusing when no device is given', async () => {
|
||||
jest
|
||||
.mocked(spawnAsync)
|
||||
.mockRejectedValueOnce(new Error('Unable to find application named Simulator'))
|
||||
.mockResolvedValueOnce({} as any);
|
||||
|
||||
await openSimulatorAppAsync('');
|
||||
|
||||
expect(spawnAsync).toHaveBeenNthCalledWith(2, 'open', ['-a', 'DeviceHub']);
|
||||
});
|
||||
|
||||
it('rethrows the Simulator.app error when Device Hub cannot be opened either', async () => {
|
||||
jest
|
||||
.mocked(spawnAsync)
|
||||
.mockRejectedValueOnce(new Error('LaunchServices failed to open Simulator'))
|
||||
.mockRejectedValueOnce(new Error('Unable to find application named DeviceHub'));
|
||||
|
||||
await expect(openSimulatorAppAsync(UDID)).rejects.toThrow(
|
||||
'LaunchServices failed to open Simulator'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(ensureSimulatorAppOpenedAsync, () => {
|
||||
it('treats a running Device Hub process as the simulator app being open', async () => {
|
||||
jest.mocked(osascript.execAsync).mockResolvedValue('1');
|
||||
|
||||
await ensureSimulatorAppOpenedAsync(UDID);
|
||||
|
||||
expect(osascript.execAsync).toHaveBeenCalledWith(
|
||||
'tell app "System Events" to count processes whose name is "Simulator" or name is "DeviceHub"'
|
||||
);
|
||||
expect(spawnAsync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('opens the simulator app when neither process is running', async () => {
|
||||
jest.mocked(osascript.execAsync).mockResolvedValueOnce('0').mockResolvedValue('1');
|
||||
jest.mocked(spawnAsync).mockResolvedValue({} as any);
|
||||
|
||||
await ensureSimulatorAppOpenedAsync(UDID);
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('open', [
|
||||
'-a',
|
||||
'Simulator',
|
||||
'--args',
|
||||
'-CurrentDeviceUDID',
|
||||
UDID,
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
|
||||
import { getSimulatorAppIdAsync } from '../simulator';
|
||||
import { validateSystemRequirementsAsync } from '../systemRequirements';
|
||||
import { getXcodeVersionAsync } from '../xcode';
|
||||
import { isXcrunInstalledAsync } from '../xcrun';
|
||||
|
||||
jest.mock('@expo/spawn-async');
|
||||
jest.mock('../simulator');
|
||||
jest.mock('../xcode');
|
||||
jest.mock('../xcrun');
|
||||
jest.mock('../../../log');
|
||||
|
||||
const originalPlatform = process.platform;
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
||||
jest.mocked(getXcodeVersionAsync).mockResolvedValue('27.0.0');
|
||||
jest.mocked(isXcrunInstalledAsync).mockResolvedValue(true);
|
||||
jest.mocked(spawnAsync).mockResolvedValue({} as any);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
||||
});
|
||||
|
||||
describe(validateSystemRequirementsAsync, () => {
|
||||
it.each([
|
||||
['Simulator.app', 'com.apple.iphonesimulator'],
|
||||
['Simulator.app trampoline', 'com.apple.CoreSimulator.SimulatorTrampoline'],
|
||||
['Device Hub', 'com.apple.dt.Devices'],
|
||||
])('accepts %s', async (_label, appId) => {
|
||||
jest.mocked(getSimulatorAppIdAsync).mockResolvedValue(appId);
|
||||
|
||||
await expect(validateSystemRequirementsAsync()).resolves.toBeUndefined();
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('xcrun', ['simctl', 'help']);
|
||||
});
|
||||
|
||||
it('rejects an unknown app id', async () => {
|
||||
jest.mocked(getSimulatorAppIdAsync).mockResolvedValue('com.example.Other');
|
||||
|
||||
await expect(validateSystemRequirementsAsync()).rejects.toThrow(
|
||||
"identified as 'com.example.Other'"
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects when no simulator host app can be found', async () => {
|
||||
jest.mocked(getSimulatorAppIdAsync).mockResolvedValue(undefined);
|
||||
|
||||
await expect(validateSystemRequirementsAsync()).rejects.toThrow(
|
||||
"Can't determine id of Device Hub or Simulator app"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as osascript from '@expo/osascript';
|
||||
import spawnAsync from '@expo/spawn-async';
|
||||
import path from 'path';
|
||||
|
||||
import { simctlAsync } from './simctl';
|
||||
import Log from '../../log';
|
||||
@@ -129,13 +130,26 @@ export async function ensureSimulatorBootedAsync(simulator: IosSimulator): Promi
|
||||
await simctlAsync(['boot', simulator.udid]);
|
||||
}
|
||||
|
||||
// TODO: once Xcode 27 is stable, try DeviceHub before Simulator here and in getSimulatorAppIdAsync,
|
||||
// in lockstep with @expo/cli (see SimulatorAppPrerequisite.ts there).
|
||||
export async function openSimulatorAppAsync(simulatorUdid: string): Promise<void> {
|
||||
const args = ['-a', 'Simulator'];
|
||||
if (simulatorUdid) {
|
||||
// This has no effect if the app is already running.
|
||||
args.push('--args', '-CurrentDeviceUDID', simulatorUdid);
|
||||
try {
|
||||
const args = ['-a', 'Simulator'];
|
||||
if (simulatorUdid) {
|
||||
// This has no effect if the app is already running.
|
||||
args.push('--args', '-CurrentDeviceUDID', simulatorUdid);
|
||||
}
|
||||
await spawnAsync('open', args);
|
||||
} catch (error) {
|
||||
const deviceHubArgs = simulatorUdid
|
||||
? [`devices://device/open?id=${simulatorUdid}`]
|
||||
: ['-a', 'DeviceHub'];
|
||||
try {
|
||||
await spawnAsync('open', deviceHubArgs);
|
||||
} catch {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await spawnAsync('open', args);
|
||||
}
|
||||
|
||||
export async function launchAppAsync(
|
||||
@@ -171,7 +185,7 @@ async function waitForSimulatorAppToStartAsync(
|
||||
async function isSimulatorAppRunningAsync(): Promise<boolean> {
|
||||
try {
|
||||
const result = await osascript.execAsync(
|
||||
'tell app "System Events" to count processes whose name is "Simulator"'
|
||||
'tell app "System Events" to count processes whose name is "Simulator" or name is "DeviceHub"'
|
||||
);
|
||||
|
||||
if (result.trim() === '0') {
|
||||
@@ -205,9 +219,41 @@ export async function installAppAsync(deviceId: string, filePath: string): Promi
|
||||
Log.succeed('Successfully installed your app on the simulator!');
|
||||
}
|
||||
|
||||
const XCODE_SIMULATOR_INFO_PLIST_PATH = './Applications/Simulator.app/Contents/Info.plist';
|
||||
const XCODE_DEVICE_HUB_INFO_PLIST_PATH = '../Applications/DeviceHub.app/Contents/Info.plist';
|
||||
|
||||
export async function getSimulatorAppIdAsync(): Promise<string | undefined> {
|
||||
// TODO: resolve DeviceHub before Simulator once Xcode 27 is stable, see openSimulatorAppAsync
|
||||
const launchServicesAppId =
|
||||
(await osascript.safeIdOfAppAsync('Simulator')) ||
|
||||
(await osascript.safeIdOfAppAsync('DeviceHub'));
|
||||
if (launchServicesAppId) {
|
||||
return launchServicesAppId;
|
||||
}
|
||||
|
||||
const xcodePath = await getXcodeSelectPathAsync();
|
||||
if (!xcodePath) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
(await getInfoPlistBundleIdAsync(path.join(xcodePath, XCODE_SIMULATOR_INFO_PLIST_PATH))) ||
|
||||
(await getInfoPlistBundleIdAsync(path.join(xcodePath, XCODE_DEVICE_HUB_INFO_PLIST_PATH)))
|
||||
);
|
||||
}
|
||||
|
||||
async function getXcodeSelectPathAsync(): Promise<string | undefined> {
|
||||
try {
|
||||
return (await osascript.execAsync('id of app "Simulator"')).trim();
|
||||
const { stdout } = await spawnAsync('xcode-select', ['--print-path']);
|
||||
return stdout.trim() || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function getInfoPlistBundleIdAsync(infoPlistPath: string): Promise<string | undefined> {
|
||||
try {
|
||||
const { stdout } = await spawnAsync('defaults', ['read', infoPlistPath, 'CFBundleIdentifier']);
|
||||
return stdout.trim() || undefined;
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -70,16 +70,17 @@ async function assertSimulatorAppInstalledAsync(): Promise<void> {
|
||||
const simulatorAppId = await getSimulatorAppIdAsync();
|
||||
if (!simulatorAppId) {
|
||||
throw new Error(
|
||||
`Can't determine id of Simulator app; the Simulator is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app/Contents/Developer'`
|
||||
`Can't determine id of Device Hub or Simulator app; it is most likely not installed on this machine. Run 'sudo xcode-select -s /Applications/Xcode.app/Contents/Developer'`
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
simulatorAppId !== 'com.apple.dt.Devices' &&
|
||||
simulatorAppId !== 'com.apple.iphonesimulator' &&
|
||||
simulatorAppId !== 'com.apple.CoreSimulator.SimulatorTrampoline'
|
||||
) {
|
||||
throw new Error(
|
||||
`Simulator is installed but is identified as '${simulatorAppId}', can't recognize what that is`
|
||||
`Device Hub or Simulator is installed but is identified as '${simulatorAppId}', can't recognize what that is`
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -2282,12 +2282,12 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@expo/apple-utils@npm:2.2.0":
|
||||
version: 2.2.0
|
||||
resolution: "@expo/apple-utils@npm:2.2.0"
|
||||
"@expo/apple-utils@npm:2.2.1":
|
||||
version: 2.2.1
|
||||
resolution: "@expo/apple-utils@npm:2.2.1"
|
||||
bin:
|
||||
apple-utils: bin.js
|
||||
checksum: 10c0/527bbe07536f5ff7aeb3774feaa4d9ed89e1ffec54f414125e9a85ba5048eb6471fa5ae6b4ce25f7387601383b4e34e2490c62d3778e12f79bff13167447187e
|
||||
checksum: 10c0/c23cac8b1b96d42dfc5f7c63a1fe1c0cb19d097b9357fd2f545c14e6cfa98725f0fca1334446b3112e66b6822ffd45ad52cb8eb4bfd29ab9f8e462abf1593cc2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -10467,7 +10467,7 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "eas-cli@workspace:packages/eas-cli"
|
||||
dependencies:
|
||||
"@expo/apple-utils": "npm:2.2.0"
|
||||
"@expo/apple-utils": "npm:2.2.1"
|
||||
"@expo/code-signing-certificates": "npm:0.0.5"
|
||||
"@expo/config": "npm:55.0.10"
|
||||
"@expo/config-plugins": "npm:55.0.7"
|
||||
|
||||
Reference in new issue
Block a user