Skip to content

Commit

Permalink
feat: add platform-cli-apple with reusable utilities for OOT platforms (
Browse files Browse the repository at this point in the history
#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
  • Loading branch information
okwasniewski authored Dec 20, 2023
1 parent 3906b90 commit a4417a1
Show file tree
Hide file tree
Showing 65 changed files with 1,358 additions and 936 deletions.
3 changes: 2 additions & 1 deletion CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ packages/cli-doctor/src/tools/healthchecks/android* @cipolleschi

# iOS
packages/cli-platform-ios/ @cipolleschi
packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi
packages/cli-platform-apple/ @cipolleschi
packages/cli-doctor/src/tools/healthchecks/ios* @cipolleschi
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ And then:

```sh
cd /my/new/react-native/project/
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"
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"
```

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`.
Expand Down
1 change: 1 addition & 0 deletions packages/cli-doctor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"@react-native-community/cli-config": "13.1.0",
"@react-native-community/cli-platform-android": "13.1.0",
"@react-native-community/cli-platform-ios": "13.1.0",
"@react-native-community/cli-platform-apple": "13.1.0",
"@react-native-community/cli-tools": "13.1.0",
"chalk": "^4.1.2",
"command-exists": "^1.2.8",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-doctor/src/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import getEnvironmentInfo from '../tools/envinfo';
import {logger, version} from '@react-native-community/cli-tools';
import {Config} from '@react-native-community/cli-types';
import {getArchitecture} from '@react-native-community/cli-platform-ios';
import {getArchitecture} from '@react-native-community/cli-platform-apple';
import {readFile} from 'fs-extra';
import path from 'path';
import {stringify} from 'yaml';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli-doctor/src/tools/healthchecks/xcodeEnv.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {findPodfilePaths} from '@react-native-community/cli-platform-ios';
import {findPodfilePaths} from '@react-native-community/cli-platform-apple';
import {
findProjectRoot,
resolveNodeModuleDir,
Expand Down
1 change: 1 addition & 0 deletions packages/cli-doctor/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
{"path": "../cli-config"},
{"path": "../cli-platform-android"},
{"path": "../cli-platform-ios"},
{"path": "../cli-platform-apple"},
]
}
42 changes: 42 additions & 0 deletions packages/cli-platform-apple/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# @react-native-community/cli-platform-apple

This package is part of the [React Native CLI](../../README.md). It contains utilities for building reusable commands targetting Apple platforms.

## Installation

```sh
yarn add @react-native-community/cli-platform-apple
```

## Usage

This package is intended to be used internally in [React Native CLI](../../README.md) and by out of tree platforms.

It exports builder commands that can be used to create custom `run-`, `log-` and `build-` commands for example: `yarn run-<oot-platform>`.

Inside of `<oot-platform>/packages/react-native/react-native.config.js`:

```js
const {
buildOptions,
createBuild,
} = require('@react-native-community/cli-platform-apple');

const buildVisionOS = {
name: 'build-visionos',
description: 'builds your app for visionOS platform',
func: createBuild({platformName: 'visionos'}),
examples: [
{
desc: 'Build the app for visionOS in Release mode',
cmd: 'npx react-native build-visionos --mode "Release"',
},
],
options: buildOptions,
};

module.exports = {
commands: [buildVisionOS], // <- Add command here
//..
};
```
35 changes: 35 additions & 0 deletions packages/cli-platform-apple/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"name": "@react-native-community/cli-platform-apple",
"version": "13.1.0",
"license": "MIT",
"main": "build/index.js",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@react-native-community/cli-tools": "13.1.0",
"chalk": "^4.1.2",
"execa": "^5.0.0",
"fast-xml-parser": "^4.0.12",
"glob": "^7.1.3",
"ora": "^5.4.1"
},
"devDependencies": {
"@react-native-community/cli-types": "13.1.0",
"@types/glob": "^7.1.1",
"@types/lodash": "^4.14.149",
"hasbin": "^1.2.3"
},
"files": [
"build",
"!*.d.ts",
"!*.map"
],
"homepage": "https://github.com/react-native-community/cli/tree/main/packages/cli-platform-apple",
"repository": {
"type": "git",
"url": "https://github.com/react-native-community/cli.git",
"directory": "packages/cli-platform-apple"
}
}

Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {writeFiles, getTempDirectory, cleanup} from '../../../../jest/helpers';
import installPods from '../tools/installPods';
import resolvePods, {compareMd5Hashes, getIosDependencies} from '../tools/pods';
import resolvePods, {
compareMd5Hashes,
getPlatformDependencies,
} from '../tools/pods';

const mockGet = jest.fn();
const mockSet = jest.fn();
Expand Down Expand Up @@ -71,9 +74,9 @@ describe('compareMd5Hashes', () => {
});
});

describe('getIosDependencies', () => {
describe('getPlatformDependencies', () => {
it('should return only dependencies with native code', () => {
const result = getIosDependencies(dependenciesConfig);
const result = getPlatformDependencies(dependenciesConfig);
expect(result).toEqual(['dep1@1.0.0', 'dep2@1.0.0']);
});
});
Expand All @@ -90,7 +93,7 @@ describe('resolvePods', () => {
it('should install pods when force option is set to true', async () => {
createTempFiles();

await resolvePods(DIR, {}, {forceInstall: true});
await resolvePods(DIR, {}, 'ios', {forceInstall: true});

expect(installPods).toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,28 @@ import {
getLoader,
} from '@react-native-community/cli-tools';
import type {BuildFlags} from './buildOptions';
import {simulatorDestinationMap} from './simulatorDestinationMap';

export function buildProject(
xcodeProject: IOSProjectInfo,
platform: string,
udid: string | undefined,
mode: string,
scheme: string,
args: BuildFlags,
): Promise<string> {
return new Promise((resolve, reject) => {
const simulatorDest = simulatorDestinationMap?.[platform];

if (!simulatorDest) {
reject(
new CLIError(
`Unknown platform: ${platform}. Please, use one of: ios, macos, visionos, tvos.`,
),
);
return;
}

const xcodebuildArgs = [
xcodeProject.isWorkspace ? '-workspace' : '-project',
xcodeProject.name,
Expand All @@ -33,8 +46,8 @@ export function buildProject(
(udid
? `id=${udid}`
: mode === 'Debug'
? 'generic/platform=iOS Simulator'
: 'generic/platform=iOS') +
? `generic/platform=${simulatorDest}`
: `generic/platform=${platform}`) +
(args.destination ? ',' + args.destination : ''),
];

Expand Down Expand Up @@ -98,7 +111,7 @@ export function buildProject(
reject(
new CLIError(
`
Failed to build iOS project.
Failed to build ${platform} project.
"xcodebuild" exited with error code '${code}'. To debug build
logs further, consider building your app with Xcode.app, by opening
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import fs from 'fs';
import {CLIError} from '@react-native-community/cli-tools';
import {Config, IOSProjectConfig} from '@react-native-community/cli-types';
import getArchitecture from '../../tools/getArchitecture';
import resolvePods from '../../tools/pods';
import {BuildFlags} from './buildOptions';
import {buildProject} from './buildProject';
import {getConfiguration} from './getConfiguration';
import {getXcodeProjectAndDir} from './getXcodeProjectAndDir';
import {BuilderCommand} from '../../types';
import findXcodeProject from '../../config/findXcodeProject';

const createBuild =
({platformName}: BuilderCommand) =>
async (_: Array<string>, ctx: Config, args: BuildFlags) => {
const platform = ctx.project[platformName] as IOSProjectConfig;
if (platform === undefined) {
throw new CLIError(`Unable to find ${platform} platform config`);
}

let {xcodeProject, sourceDir} = getXcodeProjectAndDir(platform);

let installedPods = false;
if (platform?.automaticPodsInstallation || args.forcePods) {
const isAppRunningNewArchitecture = platform?.sourceDir
? await getArchitecture(platform?.sourceDir)
: undefined;

await resolvePods(ctx.root, ctx.dependencies, platformName, {
forceInstall: args.forcePods,
newArchEnabled: isAppRunningNewArchitecture,
});

installedPods = true;
}

// if project is freshly created, revisit Xcode project to verify Pods are installed correctly.
// This is needed because ctx project is created before Pods are installed, so it might have outdated information.
if (installedPods) {
const recheckXcodeProject = findXcodeProject(fs.readdirSync(sourceDir));
if (recheckXcodeProject) {
xcodeProject = recheckXcodeProject;
}
}

process.chdir(sourceDir);

const {scheme, mode} = await getConfiguration(
xcodeProject,
sourceDir,
args,
);

return buildProject(
xcodeProject,
platformName,
undefined,
mode,
scheme,
args,
);
};

export default createBuild;
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const simulatorDestinationMap: Record<string, string> = {
ios: 'iOS Simulator',
macos: 'macOS',
visionos: 'visionOS Simulator',
tvos: 'tvOS Simulator',
};
98 changes: 98 additions & 0 deletions packages/cli-platform-apple/src/commands/logCommand/createLog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {CLIError, logger, prompt} from '@react-native-community/cli-tools';
import {Config, IOSProjectConfig} from '@react-native-community/cli-types';
import {spawnSync} from 'child_process';
import os from 'os';
import path from 'path';
import getSimulators from '../../tools/getSimulators';
import listDevices from '../../tools/listDevices';
import {getPlatformInfo} from '../runCommand/getPlatformInfo';
import {BuilderCommand} from '../../types';

/**
* Starts Apple device syslog tail
*/

type Args = {
interactive: boolean;
};

const createLog =
({platformName}: BuilderCommand) =>
async (_: Array<string>, ctx: Config, args: Args) => {
const platform = ctx.project[platformName] as IOSProjectConfig;
const {readableName: platformReadableName} = getPlatformInfo(platformName);

if (platform === undefined) {
throw new CLIError(`Unable to find ${platform} platform config`);
}

// 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.
const simulators = getSimulators();
const bootedSimulators = Object.keys(simulators.devices)
.map((key) => simulators.devices[key])
.reduce((acc, val) => acc.concat(val), [])
.filter(({state}) => state === 'Booted');

const {sdkNames} = getPlatformInfo(platformName);
const devices = await listDevices(sdkNames);

const availableSimulators = devices.filter(
({type, isAvailable}) => type === 'simulator' && isAvailable,
);

if (availableSimulators.length === 0) {
logger.error('No simulators detected. Install simulators via Xcode.');
return;
}

const bootedAndAvailableSimulators = bootedSimulators.map((booted) => {
const available = availableSimulators.find(
({udid}) => udid === booted.udid,
);
return {...available, ...booted};
});

if (bootedAndAvailableSimulators.length === 0) {
logger.error(
`No booted and available ${platformReadableName} simulators found.`,
);
return;
}

if (args.interactive && bootedAndAvailableSimulators.length > 1) {
const {udid} = await prompt({
type: 'select',
name: 'udid',
message: `Select ${platformReadableName} simulators to tail logs from`,
choices: bootedAndAvailableSimulators.map((simulator) => ({
title: simulator.name,
value: simulator.udid,
})),
});

tailDeviceLogs(udid);
} else {
tailDeviceLogs(bootedAndAvailableSimulators[0].udid);
}
};

function tailDeviceLogs(udid: string) {
const logDir = path.join(
os.homedir(),
'Library',
'Logs',
'CoreSimulator',
udid,
'asl',
);

const log = spawnSync('syslog', ['-w', '-F', 'std', '-d', logDir], {
stdio: 'inherit',
});

if (log.error !== null) {
throw log.error;
}
}

export default createLog;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export const logOptions = [
{
name: '--interactive',
description:
'Explicitly select simulator to tail logs from. By default it will tail logs from the first booted and available simulator.',
},
];
Loading

0 comments on commit a4417a1

Please sign in to comment.