diff --git a/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts b/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts index 5a20c5e47..b5762f2b9 100644 --- a/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts +++ b/packages/cli-platform-apple/src/commands/runCommand/getBuildPath.ts @@ -1,38 +1,14 @@ -import child_process from 'child_process'; -import {IOSProjectInfo} from '@react-native-community/cli-types'; -import {CLIError, logger} from '@react-native-community/cli-tools'; -import chalk from 'chalk'; +import {CLIError} from '@react-native-community/cli-tools'; +import path from 'path'; +import {BuildSettings} from './getBuildSettings'; export async function getBuildPath( - xcodeProject: IOSProjectInfo, - mode: string, - buildOutput: string, - scheme: string, - target: string | undefined, + buildSettings: BuildSettings, isCatalyst: boolean = false, ) { - const buildSettings = child_process.execFileSync( - 'xcodebuild', - [ - xcodeProject.isWorkspace ? '-workspace' : '-project', - xcodeProject.name, - '-scheme', - scheme, - '-sdk', - getPlatformName(buildOutput), - '-configuration', - mode, - '-showBuildSettings', - '-json', - ], - {encoding: 'utf8'}, - ); - - const {targetBuildDir, executableFolderPath} = await getTargetPaths( - buildSettings, - scheme, - target, - ); + const targetBuildDir = buildSettings.TARGET_BUILD_DIR; + const executableFolderPath = buildSettings.EXECUTABLE_FOLDER_PATH; + const fullProductName = buildSettings.FULL_PRODUCT_NAME; if (!targetBuildDir) { throw new CLIError('Failed to get the target build directory.'); @@ -42,63 +18,13 @@ export async function getBuildPath( throw new CLIError('Failed to get the app name.'); } - return `${targetBuildDir}${ - isCatalyst ? '-maccatalyst' : '' - }/${executableFolderPath}`; -} - -async function getTargetPaths( - buildSettings: string, - scheme: string, - target: string | undefined, -) { - const settings = JSON.parse(buildSettings); - - const targets = settings.map( - ({target: settingsTarget}: any) => settingsTarget, - ); - - let selectedTarget = targets[0]; - - if (target) { - if (!targets.includes(target)) { - logger.info( - `Target ${chalk.bold(target)} not found for scheme ${chalk.bold( - scheme, - )}, automatically selected target ${chalk.bold(selectedTarget)}`, - ); - } else { - selectedTarget = target; - } - } - - // Find app in all building settings - look for WRAPPER_EXTENSION: 'app', - - const targetIndex = targets.indexOf(selectedTarget); - - const wrapperExtension = - settings[targetIndex].buildSettings.WRAPPER_EXTENSION; - - if (wrapperExtension === 'app') { - return { - targetBuildDir: settings[targetIndex].buildSettings.TARGET_BUILD_DIR, - executableFolderPath: - settings[targetIndex].buildSettings.EXECUTABLE_FOLDER_PATH, - }; + if (!fullProductName) { + throw new CLIError('Failed to get product name.'); } - return {}; -} - -function getPlatformName(buildOutput: string) { - // Xcode can sometimes escape `=` with a backslash or put the value in quotes - const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec( - buildOutput, - ); - if (!platformNameMatch) { - throw new CLIError( - 'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.', - ); + if (isCatalyst) { + return path.join(targetBuildDir, '-maccatalyst', executableFolderPath); + } else { + return path.join(targetBuildDir, executableFolderPath); } - return platformNameMatch[1]; } diff --git a/packages/cli-platform-apple/src/commands/runCommand/getBuildSettings.ts b/packages/cli-platform-apple/src/commands/runCommand/getBuildSettings.ts new file mode 100644 index 000000000..923c9d277 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/getBuildSettings.ts @@ -0,0 +1,81 @@ +import {CLIError, logger} from '@react-native-community/cli-tools'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import chalk from 'chalk'; +import child_process from 'child_process'; + +export type BuildSettings = { + TARGET_BUILD_DIR: string; + INFOPLIST_PATH: string; + EXECUTABLE_FOLDER_PATH: string; + FULL_PRODUCT_NAME: string; +}; + +export async function getBuildSettings( + xcodeProject: IOSProjectInfo, + mode: string, + buildOutput: string, + scheme: string, + target?: string, +): Promise { + const buildSettings = child_process.execFileSync( + 'xcodebuild', + [ + xcodeProject.isWorkspace ? '-workspace' : '-project', + xcodeProject.name, + '-scheme', + scheme, + '-sdk', + getPlatformName(buildOutput), + '-configuration', + mode, + '-showBuildSettings', + '-json', + ], + {encoding: 'utf8'}, + ); + + const settings = JSON.parse(buildSettings); + + const targets = settings.map( + ({target: settingsTarget}: any) => settingsTarget, + ); + + let selectedTarget = targets[0]; + + if (target) { + if (!targets.includes(target)) { + logger.info( + `Target ${chalk.bold(target)} not found for scheme ${chalk.bold( + scheme, + )}, automatically selected target ${chalk.bold(selectedTarget)}`, + ); + } else { + selectedTarget = target; + } + } + + // Find app in all building settings - look for WRAPPER_EXTENSION: 'app', + const targetIndex = targets.indexOf(selectedTarget); + const targetSettings = settings[targetIndex].buildSettings; + + const wrapperExtension = targetSettings.WRAPPER_EXTENSION; + + if (wrapperExtension === 'app') { + return settings[targetIndex].buildSettings; + } + + return null; +} + +function getPlatformName(buildOutput: string) { + // Xcode can sometimes escape `=` with a backslash or put the value in quotes + const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec( + buildOutput, + ); + if (!platformNameMatch) { + throw new CLIError( + 'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.', + ); + } + return platformNameMatch[1]; +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/installApp.ts b/packages/cli-platform-apple/src/commands/runCommand/installApp.ts new file mode 100644 index 000000000..cd5088452 --- /dev/null +++ b/packages/cli-platform-apple/src/commands/runCommand/installApp.ts @@ -0,0 +1,107 @@ +import child_process from 'child_process'; +import {CLIError, logger} from '@react-native-community/cli-tools'; +import {IOSProjectInfo} from '@react-native-community/cli-types'; +import chalk from 'chalk'; +import {getBuildPath} from './getBuildPath'; +import {getBuildSettings} from './getBuildSettings'; +import path from 'path'; + +function handleLaunchResult( + success: boolean, + errorMessage: string, + errorDetails = '', +) { + if (success) { + logger.success('Successfully launched the app'); + } else { + logger.error(errorMessage, errorDetails); + } +} + +type Options = { + buildOutput: any; + xcodeProject: IOSProjectInfo; + mode: string; + scheme: string; + target?: string; + udid: string; + binaryPath?: string; +}; + +export default async function installApp({ + buildOutput, + xcodeProject, + mode, + scheme, + target, + udid, + binaryPath, +}: Options) { + let appPath = binaryPath; + + const buildSettings = await getBuildSettings( + xcodeProject, + mode, + buildOutput, + scheme, + target, + ); + + if (!buildSettings) { + throw new CLIError('Failed to get build settings for your project'); + } + + if (!appPath) { + appPath = await getBuildPath(buildSettings); + } + + if (!buildSettings) { + throw new CLIError('Failed to get build settings for your project'); + } + + const targetBuildDir = buildSettings.TARGET_BUILD_DIR; + const infoPlistPath = buildSettings.INFOPLIST_PATH; + + if (!infoPlistPath) { + throw new CLIError('Failed to find Info.plist'); + } + + if (!targetBuildDir) { + throw new CLIError('Failed to get target build directory.'); + } + + logger.info(`Installing "${chalk.bold(appPath)}`); + + if (udid && appPath) { + child_process.spawnSync('xcrun', ['simctl', 'install', udid, appPath], { + stdio: 'inherit', + }); + } + + const bundleID = child_process + .execFileSync( + '/usr/libexec/PlistBuddy', + [ + '-c', + 'Print:CFBundleIdentifier', + path.join(targetBuildDir, infoPlistPath), + ], + {encoding: 'utf8'}, + ) + .trim(); + + logger.info(`Launching "${chalk.bold(bundleID)}"`); + + let result = child_process.spawnSync('xcrun', [ + 'simctl', + 'launch', + udid, + bundleID, + ]); + + handleLaunchResult( + result.status === 0, + 'Failed to launch the app on simulator', + result.stderr.toString(), + ); +} diff --git a/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts b/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts index 46d463b85..6bfd263e4 100644 --- a/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts +++ b/packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts @@ -6,6 +6,7 @@ import chalk from 'chalk'; import {buildProject} from '../buildCommand/buildProject'; import {getBuildPath} from './getBuildPath'; import {FlagsT} from './createRun'; +import {getBuildSettings} from './getBuildSettings'; export async function runOnDevice( selectedDevice: Device, @@ -45,14 +46,18 @@ export async function runOnDevice( args, ); - const appPath = await getBuildPath( + const buildSettings = await getBuildSettings( xcodeProject, mode, buildOutput, scheme, - args.target, - true, ); + + if (!buildSettings) { + throw new CLIError('Failed to get build settings for your project'); + } + + const appPath = await getBuildPath(buildSettings, true); const appProcess = child_process.spawn(`${appPath}/${scheme}`, [], { detached: true, stdio: 'ignore', @@ -70,13 +75,18 @@ export async function runOnDevice( args, ); - appPath = await getBuildPath( + const buildSettings = await getBuildSettings( xcodeProject, mode, buildOutput, scheme, - args.target, ); + + if (!buildSettings) { + throw new CLIError('Failed to get build settings for your project'); + } + + appPath = await getBuildPath(buildSettings); } else { appPath = args.binaryPath; } diff --git a/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts b/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts index 1cd676aca..88c25755b 100644 --- a/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts +++ b/packages/cli-platform-apple/src/commands/runCommand/runOnSimulator.ts @@ -1,13 +1,11 @@ import child_process from 'child_process'; import {IOSProjectInfo} from '@react-native-community/cli-types'; -import path from 'path'; import {logger} from '@react-native-community/cli-tools'; -import chalk from 'chalk'; import {ApplePlatform, Device} from '../../types'; import {buildProject} from '../buildCommand/buildProject'; import {formattedDeviceName} from './matchingDevice'; -import {getBuildPath} from './getBuildPath'; import {FlagsT} from './createRun'; +import installApp from './installApp'; export async function runOnSimulator( xcodeProject: IOSProjectInfo, @@ -17,6 +15,8 @@ export async function runOnSimulator( args: FlagsT, simulator: Device, ) { + const {binaryPath, target} = args; + /** * Booting simulator through `xcrun simctl boot` will boot it in the `headless` mode * (running in the background). @@ -43,8 +43,8 @@ export async function runOnSimulator( bootSimulator(simulator); } - let buildOutput, appPath; - if (!args.binaryPath) { + let buildOutput; + if (!binaryPath) { buildOutput = await buildProject( xcodeProject, platform, @@ -53,51 +53,17 @@ export async function runOnSimulator( scheme, args, ); - - appPath = await getBuildPath( - xcodeProject, - mode, - buildOutput, - scheme, - args.target, - ); - } else { - appPath = args.binaryPath; } - logger.info(`Installing "${chalk.bold(appPath)} on ${simulator.name}"`); - - child_process.spawnSync( - 'xcrun', - ['simctl', 'install', simulator.udid, appPath], - {stdio: 'inherit'}, - ); - - const bundleID = child_process - .execFileSync( - '/usr/libexec/PlistBuddy', - ['-c', 'Print:CFBundleIdentifier', path.join(appPath, 'Info.plist')], - {encoding: 'utf8'}, - ) - .trim(); - - logger.info(`Launching "${chalk.bold(bundleID)}"`); - - const result = child_process.spawnSync('xcrun', [ - 'simctl', - 'launch', - simulator.udid, - bundleID, - ]); - - if (result.status === 0) { - logger.success('Successfully launched the app on the simulator'); - } else { - logger.error( - 'Failed to launch the app on simulator', - result.stderr.toString(), - ); - } + installApp({ + buildOutput, + xcodeProject, + mode, + scheme, + target, + udid: simulator.udid, + binaryPath, + }); } function bootSimulator(selectedSimulator: Device) {