Skip to content

Commit 296e640

Browse files
refactor(cli-platform-apple): move installing and launching app logic to separate function (#2234)
* refactor: move installing and launching app logic to separate function * fix: apply code reviews suggestions
1 parent a36eefc commit 296e640

File tree

5 files changed

+230
-140
lines changed

5 files changed

+230
-140
lines changed
Lines changed: 13 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,14 @@
1-
import child_process from 'child_process';
2-
import {IOSProjectInfo} from '@react-native-community/cli-types';
3-
import {CLIError, logger} from '@react-native-community/cli-tools';
4-
import chalk from 'chalk';
1+
import {CLIError} from '@react-native-community/cli-tools';
2+
import path from 'path';
3+
import {BuildSettings} from './getBuildSettings';
54

65
export async function getBuildPath(
7-
xcodeProject: IOSProjectInfo,
8-
mode: string,
9-
buildOutput: string,
10-
scheme: string,
11-
target: string | undefined,
6+
buildSettings: BuildSettings,
127
isCatalyst: boolean = false,
138
) {
14-
const buildSettings = child_process.execFileSync(
15-
'xcodebuild',
16-
[
17-
xcodeProject.isWorkspace ? '-workspace' : '-project',
18-
xcodeProject.name,
19-
'-scheme',
20-
scheme,
21-
'-sdk',
22-
getPlatformName(buildOutput),
23-
'-configuration',
24-
mode,
25-
'-showBuildSettings',
26-
'-json',
27-
],
28-
{encoding: 'utf8'},
29-
);
30-
31-
const {targetBuildDir, executableFolderPath} = await getTargetPaths(
32-
buildSettings,
33-
scheme,
34-
target,
35-
);
9+
const targetBuildDir = buildSettings.TARGET_BUILD_DIR;
10+
const executableFolderPath = buildSettings.EXECUTABLE_FOLDER_PATH;
11+
const fullProductName = buildSettings.FULL_PRODUCT_NAME;
3612

3713
if (!targetBuildDir) {
3814
throw new CLIError('Failed to get the target build directory.');
@@ -42,63 +18,13 @@ export async function getBuildPath(
4218
throw new CLIError('Failed to get the app name.');
4319
}
4420

45-
return `${targetBuildDir}${
46-
isCatalyst ? '-maccatalyst' : ''
47-
}/${executableFolderPath}`;
48-
}
49-
50-
async function getTargetPaths(
51-
buildSettings: string,
52-
scheme: string,
53-
target: string | undefined,
54-
) {
55-
const settings = JSON.parse(buildSettings);
56-
57-
const targets = settings.map(
58-
({target: settingsTarget}: any) => settingsTarget,
59-
);
60-
61-
let selectedTarget = targets[0];
62-
63-
if (target) {
64-
if (!targets.includes(target)) {
65-
logger.info(
66-
`Target ${chalk.bold(target)} not found for scheme ${chalk.bold(
67-
scheme,
68-
)}, automatically selected target ${chalk.bold(selectedTarget)}`,
69-
);
70-
} else {
71-
selectedTarget = target;
72-
}
73-
}
74-
75-
// Find app in all building settings - look for WRAPPER_EXTENSION: 'app',
76-
77-
const targetIndex = targets.indexOf(selectedTarget);
78-
79-
const wrapperExtension =
80-
settings[targetIndex].buildSettings.WRAPPER_EXTENSION;
81-
82-
if (wrapperExtension === 'app') {
83-
return {
84-
targetBuildDir: settings[targetIndex].buildSettings.TARGET_BUILD_DIR,
85-
executableFolderPath:
86-
settings[targetIndex].buildSettings.EXECUTABLE_FOLDER_PATH,
87-
};
21+
if (!fullProductName) {
22+
throw new CLIError('Failed to get product name.');
8823
}
8924

90-
return {};
91-
}
92-
93-
function getPlatformName(buildOutput: string) {
94-
// Xcode can sometimes escape `=` with a backslash or put the value in quotes
95-
const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec(
96-
buildOutput,
97-
);
98-
if (!platformNameMatch) {
99-
throw new CLIError(
100-
'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.',
101-
);
25+
if (isCatalyst) {
26+
return path.join(targetBuildDir, '-maccatalyst', executableFolderPath);
27+
} else {
28+
return path.join(targetBuildDir, executableFolderPath);
10229
}
103-
return platformNameMatch[1];
10430
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import {CLIError, logger} from '@react-native-community/cli-tools';
2+
import {IOSProjectInfo} from '@react-native-community/cli-types';
3+
import chalk from 'chalk';
4+
import child_process from 'child_process';
5+
6+
export type BuildSettings = {
7+
TARGET_BUILD_DIR: string;
8+
INFOPLIST_PATH: string;
9+
EXECUTABLE_FOLDER_PATH: string;
10+
FULL_PRODUCT_NAME: string;
11+
};
12+
13+
export async function getBuildSettings(
14+
xcodeProject: IOSProjectInfo,
15+
mode: string,
16+
buildOutput: string,
17+
scheme: string,
18+
target?: string,
19+
): Promise<BuildSettings | null> {
20+
const buildSettings = child_process.execFileSync(
21+
'xcodebuild',
22+
[
23+
xcodeProject.isWorkspace ? '-workspace' : '-project',
24+
xcodeProject.name,
25+
'-scheme',
26+
scheme,
27+
'-sdk',
28+
getPlatformName(buildOutput),
29+
'-configuration',
30+
mode,
31+
'-showBuildSettings',
32+
'-json',
33+
],
34+
{encoding: 'utf8'},
35+
);
36+
37+
const settings = JSON.parse(buildSettings);
38+
39+
const targets = settings.map(
40+
({target: settingsTarget}: any) => settingsTarget,
41+
);
42+
43+
let selectedTarget = targets[0];
44+
45+
if (target) {
46+
if (!targets.includes(target)) {
47+
logger.info(
48+
`Target ${chalk.bold(target)} not found for scheme ${chalk.bold(
49+
scheme,
50+
)}, automatically selected target ${chalk.bold(selectedTarget)}`,
51+
);
52+
} else {
53+
selectedTarget = target;
54+
}
55+
}
56+
57+
// Find app in all building settings - look for WRAPPER_EXTENSION: 'app',
58+
const targetIndex = targets.indexOf(selectedTarget);
59+
const targetSettings = settings[targetIndex].buildSettings;
60+
61+
const wrapperExtension = targetSettings.WRAPPER_EXTENSION;
62+
63+
if (wrapperExtension === 'app') {
64+
return settings[targetIndex].buildSettings;
65+
}
66+
67+
return null;
68+
}
69+
70+
function getPlatformName(buildOutput: string) {
71+
// Xcode can sometimes escape `=` with a backslash or put the value in quotes
72+
const platformNameMatch = /export PLATFORM_NAME\\?="?(\w+)"?$/m.exec(
73+
buildOutput,
74+
);
75+
if (!platformNameMatch) {
76+
throw new CLIError(
77+
'Couldn\'t find "PLATFORM_NAME" variable in xcodebuild output. Please report this issue and run your project with Xcode instead.',
78+
);
79+
}
80+
return platformNameMatch[1];
81+
}
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import child_process from 'child_process';
2+
import {CLIError, logger} from '@react-native-community/cli-tools';
3+
import {IOSProjectInfo} from '@react-native-community/cli-types';
4+
import chalk from 'chalk';
5+
import {getBuildPath} from './getBuildPath';
6+
import {getBuildSettings} from './getBuildSettings';
7+
import path from 'path';
8+
9+
function handleLaunchResult(
10+
success: boolean,
11+
errorMessage: string,
12+
errorDetails = '',
13+
) {
14+
if (success) {
15+
logger.success('Successfully launched the app');
16+
} else {
17+
logger.error(errorMessage, errorDetails);
18+
}
19+
}
20+
21+
type Options = {
22+
buildOutput: any;
23+
xcodeProject: IOSProjectInfo;
24+
mode: string;
25+
scheme: string;
26+
target?: string;
27+
udid: string;
28+
binaryPath?: string;
29+
};
30+
31+
export default async function installApp({
32+
buildOutput,
33+
xcodeProject,
34+
mode,
35+
scheme,
36+
target,
37+
udid,
38+
binaryPath,
39+
}: Options) {
40+
let appPath = binaryPath;
41+
42+
const buildSettings = await getBuildSettings(
43+
xcodeProject,
44+
mode,
45+
buildOutput,
46+
scheme,
47+
target,
48+
);
49+
50+
if (!buildSettings) {
51+
throw new CLIError('Failed to get build settings for your project');
52+
}
53+
54+
if (!appPath) {
55+
appPath = await getBuildPath(buildSettings);
56+
}
57+
58+
if (!buildSettings) {
59+
throw new CLIError('Failed to get build settings for your project');
60+
}
61+
62+
const targetBuildDir = buildSettings.TARGET_BUILD_DIR;
63+
const infoPlistPath = buildSettings.INFOPLIST_PATH;
64+
65+
if (!infoPlistPath) {
66+
throw new CLIError('Failed to find Info.plist');
67+
}
68+
69+
if (!targetBuildDir) {
70+
throw new CLIError('Failed to get target build directory.');
71+
}
72+
73+
logger.info(`Installing "${chalk.bold(appPath)}`);
74+
75+
if (udid && appPath) {
76+
child_process.spawnSync('xcrun', ['simctl', 'install', udid, appPath], {
77+
stdio: 'inherit',
78+
});
79+
}
80+
81+
const bundleID = child_process
82+
.execFileSync(
83+
'/usr/libexec/PlistBuddy',
84+
[
85+
'-c',
86+
'Print:CFBundleIdentifier',
87+
path.join(targetBuildDir, infoPlistPath),
88+
],
89+
{encoding: 'utf8'},
90+
)
91+
.trim();
92+
93+
logger.info(`Launching "${chalk.bold(bundleID)}"`);
94+
95+
let result = child_process.spawnSync('xcrun', [
96+
'simctl',
97+
'launch',
98+
udid,
99+
bundleID,
100+
]);
101+
102+
handleLaunchResult(
103+
result.status === 0,
104+
'Failed to launch the app on simulator',
105+
result.stderr.toString(),
106+
);
107+
}

packages/cli-platform-apple/src/commands/runCommand/runOnDevice.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import chalk from 'chalk';
66
import {buildProject} from '../buildCommand/buildProject';
77
import {getBuildPath} from './getBuildPath';
88
import {FlagsT} from './createRun';
9+
import {getBuildSettings} from './getBuildSettings';
910

1011
export async function runOnDevice(
1112
selectedDevice: Device,
@@ -45,14 +46,18 @@ export async function runOnDevice(
4546
args,
4647
);
4748

48-
const appPath = await getBuildPath(
49+
const buildSettings = await getBuildSettings(
4950
xcodeProject,
5051
mode,
5152
buildOutput,
5253
scheme,
53-
args.target,
54-
true,
5554
);
55+
56+
if (!buildSettings) {
57+
throw new CLIError('Failed to get build settings for your project');
58+
}
59+
60+
const appPath = await getBuildPath(buildSettings, true);
5661
const appProcess = child_process.spawn(`${appPath}/${scheme}`, [], {
5762
detached: true,
5863
stdio: 'ignore',
@@ -70,13 +75,18 @@ export async function runOnDevice(
7075
args,
7176
);
7277

73-
appPath = await getBuildPath(
78+
const buildSettings = await getBuildSettings(
7479
xcodeProject,
7580
mode,
7681
buildOutput,
7782
scheme,
78-
args.target,
7983
);
84+
85+
if (!buildSettings) {
86+
throw new CLIError('Failed to get build settings for your project');
87+
}
88+
89+
appPath = await getBuildPath(buildSettings);
8090
} else {
8191
appPath = args.binaryPath;
8292
}

0 commit comments

Comments
 (0)