Skip to content

Commit a4417a1

Browse files
authored
feat: add platform-cli-apple with reusable utilities for OOT platforms (react-native-community#2208)
* feat: refactor run-ios to separate files, export more utilities * [wip] feat: use builder pattern to easily reuse commands for OOT platforms * feat: add package * docs: document cli-platform-apple * fix: move generated files * fix: indentation * fix: building packages * fix: tests * fix: account for macOS in simulatorDest * fix: recheck pods for build command * feat: add getProjectConfig for OOT platforms * feat: fallback to first available device * refactor: use platformInfo utility * fix: bring back podspecs * fix: apply reviewers comments
1 parent 3906b90 commit a4417a1

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

65 files changed

+1358
-936
lines changed

CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ packages/cli-doctor/src/tools/healthchecks/android* @cipolleschi
66

77
# iOS
88
packages/cli-platform-ios/ @cipolleschi
9-
packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi
9+
packages/cli-platform-apple/ @cipolleschi
10+
packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ And then:
3535

3636
```sh
3737
cd /my/new/react-native/project/
38-
yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config"
38+
yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-clean" "@react-native-community/cli-doctor" "@react-native-community/cli-config" "@react-native-community/cli-platform-apple"
3939
```
4040

4141
Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`.

packages/cli-doctor/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"@react-native-community/cli-config": "13.1.0",
1212
"@react-native-community/cli-platform-android": "13.1.0",
1313
"@react-native-community/cli-platform-ios": "13.1.0",
14+
"@react-native-community/cli-platform-apple": "13.1.0",
1415
"@react-native-community/cli-tools": "13.1.0",
1516
"chalk": "^4.1.2",
1617
"command-exists": "^1.2.8",

packages/cli-doctor/src/commands/info.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import getEnvironmentInfo from '../tools/envinfo';
99
import {logger, version} from '@react-native-community/cli-tools';
1010
import {Config} from '@react-native-community/cli-types';
11-
import {getArchitecture} from '@react-native-community/cli-platform-ios';
11+
import {getArchitecture} from '@react-native-community/cli-platform-apple';
1212
import {readFile} from 'fs-extra';
1313
import path from 'path';
1414
import {stringify} from 'yaml';

packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {findPodfilePaths} from '@react-native-community/cli-platform-ios';
1+
import {findPodfilePaths} from '@react-native-community/cli-platform-apple';
22
import {
33
findProjectRoot,
44
resolveNodeModuleDir,

packages/cli-doctor/tsconfig.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,6 @@
1010
{"path": "../cli-config"},
1111
{"path": "../cli-platform-android"},
1212
{"path": "../cli-platform-ios"},
13+
{"path": "../cli-platform-apple"},
1314
]
1415
}

packages/cli-platform-apple/README.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
# @react-native-community/cli-platform-apple
2+
3+
This package is part of the [React Native CLI](../../README.md). It contains utilities for building reusable commands targetting Apple platforms.
4+
5+
## Installation
6+
7+
```sh
8+
yarn add @react-native-community/cli-platform-apple
9+
```
10+
11+
## Usage
12+
13+
This package is intended to be used internally in [React Native CLI](../../README.md) and by out of tree platforms.
14+
15+
It exports builder commands that can be used to create custom `run-`, `log-` and `build-` commands for example: `yarn run-<oot-platform>`.
16+
17+
Inside of `<oot-platform>/packages/react-native/react-native.config.js`:
18+
19+
```js
20+
const {
21+
buildOptions,
22+
createBuild,
23+
} = require('@react-native-community/cli-platform-apple');
24+
25+
const buildVisionOS = {
26+
name: 'build-visionos',
27+
description: 'builds your app for visionOS platform',
28+
func: createBuild({platformName: 'visionos'}),
29+
examples: [
30+
{
31+
desc: 'Build the app for visionOS in Release mode',
32+
cmd: 'npx react-native build-visionos --mode "Release"',
33+
},
34+
],
35+
options: buildOptions,
36+
};
37+
38+
module.exports = {
39+
commands: [buildVisionOS], // <- Add command here
40+
//..
41+
};
42+
```
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "@react-native-community/cli-platform-apple",
3+
"version": "13.1.0",
4+
"license": "MIT",
5+
"main": "build/index.js",
6+
"publishConfig": {
7+
"access": "public"
8+
},
9+
"dependencies": {
10+
"@react-native-community/cli-tools": "13.1.0",
11+
"chalk": "^4.1.2",
12+
"execa": "^5.0.0",
13+
"fast-xml-parser": "^4.0.12",
14+
"glob": "^7.1.3",
15+
"ora": "^5.4.1"
16+
},
17+
"devDependencies": {
18+
"@react-native-community/cli-types": "13.1.0",
19+
"@types/glob": "^7.1.1",
20+
"@types/lodash": "^4.14.149",
21+
"hasbin": "^1.2.3"
22+
},
23+
"files": [
24+
"build",
25+
"!*.d.ts",
26+
"!*.map"
27+
],
28+
"homepage": "https://github.com/react-native-community/cli/tree/main/packages/cli-platform-apple",
29+
"repository": {
30+
"type": "git",
31+
"url": "https://github.com/react-native-community/cli.git",
32+
"directory": "packages/cli-platform-apple"
33+
}
34+
}
35+

packages/cli-platform-ios/src/__tests__/pods.test.ts renamed to packages/cli-platform-apple/src/__tests__/pods.test.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {writeFiles, getTempDirectory, cleanup} from '../../../../jest/helpers';
22
import installPods from '../tools/installPods';
3-
import resolvePods, {compareMd5Hashes, getIosDependencies} from '../tools/pods';
3+
import resolvePods, {
4+
compareMd5Hashes,
5+
getPlatformDependencies,
6+
} from '../tools/pods';
47

58
const mockGet = jest.fn();
69
const mockSet = jest.fn();
@@ -71,9 +74,9 @@ describe('compareMd5Hashes', () => {
7174
});
7275
});
7376

74-
describe('getIosDependencies', () => {
77+
describe('getPlatformDependencies', () => {
7578
it('should return only dependencies with native code', () => {
76-
const result = getIosDependencies(dependenciesConfig);
79+
const result = getPlatformDependencies(dependenciesConfig);
7780
expect(result).toEqual(['[email protected]', '[email protected]']);
7881
});
7982
});
@@ -90,7 +93,7 @@ describe('resolvePods', () => {
9093
it('should install pods when force option is set to true', async () => {
9194
createTempFiles();
9295

93-
await resolvePods(DIR, {}, {forceInstall: true});
96+
await resolvePods(DIR, {}, 'ios', {forceInstall: true});
9497

9598
expect(installPods).toHaveBeenCalled();
9699
});

packages/cli-platform-ios/src/commands/buildIOS/buildProject.ts renamed to packages/cli-platform-apple/src/commands/buildCommand/buildProject.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,28 @@ import {
1111
getLoader,
1212
} from '@react-native-community/cli-tools';
1313
import type {BuildFlags} from './buildOptions';
14+
import {simulatorDestinationMap} from './simulatorDestinationMap';
1415

1516
export function buildProject(
1617
xcodeProject: IOSProjectInfo,
18+
platform: string,
1719
udid: string | undefined,
1820
mode: string,
1921
scheme: string,
2022
args: BuildFlags,
2123
): Promise<string> {
2224
return new Promise((resolve, reject) => {
25+
const simulatorDest = simulatorDestinationMap?.[platform];
26+
27+
if (!simulatorDest) {
28+
reject(
29+
new CLIError(
30+
`Unknown platform: ${platform}. Please, use one of: ios, macos, visionos, tvos.`,
31+
),
32+
);
33+
return;
34+
}
35+
2336
const xcodebuildArgs = [
2437
xcodeProject.isWorkspace ? '-workspace' : '-project',
2538
xcodeProject.name,
@@ -33,8 +46,8 @@ export function buildProject(
3346
(udid
3447
? `id=${udid}`
3548
: mode === 'Debug'
36-
? 'generic/platform=iOS Simulator'
37-
: 'generic/platform=iOS') +
49+
? `generic/platform=${simulatorDest}`
50+
: `generic/platform=${platform}`) +
3851
(args.destination ? ',' + args.destination : ''),
3952
];
4053

@@ -98,7 +111,7 @@ export function buildProject(
98111
reject(
99112
new CLIError(
100113
`
101-
Failed to build iOS project.
114+
Failed to build ${platform} project.
102115
103116
"xcodebuild" exited with error code '${code}'. To debug build
104117
logs further, consider building your app with Xcode.app, by opening
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import fs from 'fs';
2+
import {CLIError} from '@react-native-community/cli-tools';
3+
import {Config, IOSProjectConfig} from '@react-native-community/cli-types';
4+
import getArchitecture from '../../tools/getArchitecture';
5+
import resolvePods from '../../tools/pods';
6+
import {BuildFlags} from './buildOptions';
7+
import {buildProject} from './buildProject';
8+
import {getConfiguration} from './getConfiguration';
9+
import {getXcodeProjectAndDir} from './getXcodeProjectAndDir';
10+
import {BuilderCommand} from '../../types';
11+
import findXcodeProject from '../../config/findXcodeProject';
12+
13+
const createBuild =
14+
({platformName}: BuilderCommand) =>
15+
async (_: Array<string>, ctx: Config, args: BuildFlags) => {
16+
const platform = ctx.project[platformName] as IOSProjectConfig;
17+
if (platform === undefined) {
18+
throw new CLIError(`Unable to find ${platform} platform config`);
19+
}
20+
21+
let {xcodeProject, sourceDir} = getXcodeProjectAndDir(platform);
22+
23+
let installedPods = false;
24+
if (platform?.automaticPodsInstallation || args.forcePods) {
25+
const isAppRunningNewArchitecture = platform?.sourceDir
26+
? await getArchitecture(platform?.sourceDir)
27+
: undefined;
28+
29+
await resolvePods(ctx.root, ctx.dependencies, platformName, {
30+
forceInstall: args.forcePods,
31+
newArchEnabled: isAppRunningNewArchitecture,
32+
});
33+
34+
installedPods = true;
35+
}
36+
37+
// if project is freshly created, revisit Xcode project to verify Pods are installed correctly.
38+
// This is needed because ctx project is created before Pods are installed, so it might have outdated information.
39+
if (installedPods) {
40+
const recheckXcodeProject = findXcodeProject(fs.readdirSync(sourceDir));
41+
if (recheckXcodeProject) {
42+
xcodeProject = recheckXcodeProject;
43+
}
44+
}
45+
46+
process.chdir(sourceDir);
47+
48+
const {scheme, mode} = await getConfiguration(
49+
xcodeProject,
50+
sourceDir,
51+
args,
52+
);
53+
54+
return buildProject(
55+
xcodeProject,
56+
platformName,
57+
undefined,
58+
mode,
59+
scheme,
60+
args,
61+
);
62+
};
63+
64+
export default createBuild;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const simulatorDestinationMap: Record<string, string> = {
2+
ios: 'iOS Simulator',
3+
macos: 'macOS',
4+
visionos: 'visionOS Simulator',
5+
tvos: 'tvOS Simulator',
6+
};
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import {CLIError, logger, prompt} from '@react-native-community/cli-tools';
2+
import {Config, IOSProjectConfig} from '@react-native-community/cli-types';
3+
import {spawnSync} from 'child_process';
4+
import os from 'os';
5+
import path from 'path';
6+
import getSimulators from '../../tools/getSimulators';
7+
import listDevices from '../../tools/listDevices';
8+
import {getPlatformInfo} from '../runCommand/getPlatformInfo';
9+
import {BuilderCommand} from '../../types';
10+
11+
/**
12+
* Starts Apple device syslog tail
13+
*/
14+
15+
type Args = {
16+
interactive: boolean;
17+
};
18+
19+
const createLog =
20+
({platformName}: BuilderCommand) =>
21+
async (_: Array<string>, ctx: Config, args: Args) => {
22+
const platform = ctx.project[platformName] as IOSProjectConfig;
23+
const {readableName: platformReadableName} = getPlatformInfo(platformName);
24+
25+
if (platform === undefined) {
26+
throw new CLIError(`Unable to find ${platform} platform config`);
27+
}
28+
29+
// Here we're using two command because first command `xcrun simctl list --json devices` outputs `state` but doesn't return `available`. But second command `xcrun xcdevice list` outputs `available` but doesn't output `state`. So we need to connect outputs of both commands.
30+
const simulators = getSimulators();
31+
const bootedSimulators = Object.keys(simulators.devices)
32+
.map((key) => simulators.devices[key])
33+
.reduce((acc, val) => acc.concat(val), [])
34+
.filter(({state}) => state === 'Booted');
35+
36+
const {sdkNames} = getPlatformInfo(platformName);
37+
const devices = await listDevices(sdkNames);
38+
39+
const availableSimulators = devices.filter(
40+
({type, isAvailable}) => type === 'simulator' && isAvailable,
41+
);
42+
43+
if (availableSimulators.length === 0) {
44+
logger.error('No simulators detected. Install simulators via Xcode.');
45+
return;
46+
}
47+
48+
const bootedAndAvailableSimulators = bootedSimulators.map((booted) => {
49+
const available = availableSimulators.find(
50+
({udid}) => udid === booted.udid,
51+
);
52+
return {...available, ...booted};
53+
});
54+
55+
if (bootedAndAvailableSimulators.length === 0) {
56+
logger.error(
57+
`No booted and available ${platformReadableName} simulators found.`,
58+
);
59+
return;
60+
}
61+
62+
if (args.interactive && bootedAndAvailableSimulators.length > 1) {
63+
const {udid} = await prompt({
64+
type: 'select',
65+
name: 'udid',
66+
message: `Select ${platformReadableName} simulators to tail logs from`,
67+
choices: bootedAndAvailableSimulators.map((simulator) => ({
68+
title: simulator.name,
69+
value: simulator.udid,
70+
})),
71+
});
72+
73+
tailDeviceLogs(udid);
74+
} else {
75+
tailDeviceLogs(bootedAndAvailableSimulators[0].udid);
76+
}
77+
};
78+
79+
function tailDeviceLogs(udid: string) {
80+
const logDir = path.join(
81+
os.homedir(),
82+
'Library',
83+
'Logs',
84+
'CoreSimulator',
85+
udid,
86+
'asl',
87+
);
88+
89+
const log = spawnSync('syslog', ['-w', '-F', 'std', '-d', logDir], {
90+
stdio: 'inherit',
91+
});
92+
93+
if (log.error !== null) {
94+
throw log.error;
95+
}
96+
}
97+
98+
export default createLog;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const logOptions = [
2+
{
3+
name: '--interactive',
4+
description:
5+
'Explicitly select simulator to tail logs from. By default it will tail logs from the first booted and available simulator.',
6+
},
7+
];

0 commit comments

Comments
 (0)