Skip to content
This repository has been archived by the owner on Jan 18, 2024. It is now read-only.

Commit

Permalink
created ios command (#3303)
Browse files Browse the repository at this point in the history
* [wip] expo ios -- local iOS build command

* clean up

* remove cocoapods stuff

* Remove XCPretty JS bundle error logging optimization

* write logs to file

* wip

* update

* Update yarn.lock

* fix launching

* added code signing

* remove old xcpretty

* updated format

* wip

* refactor

* fix commands

* refactor formatter

* Added support for catching metro bundle errors

* Update ExpoLogFormatter.ts

* updated xcpretty

* refactor

* Added cocoapods stuff back

* Remove PlistBuddy and add WIP LLDB

* Improve bplist

* Added simulator log stream

* silence more errors

* refactor

* Update ExpoLogFormatter.ts

* cleanup ios build command

* Update index.ts

* added dynamic port resolver

* Update index.ts

* Delete code signing plugins

* [wip] prevent opening bundler

* refactor

* move logs

* Update Simulator.ts

* added --bundler flag

* revert changes

* Update Podfile.ts

* added unit test for logger
  • Loading branch information
EvanBacon authored Mar 19, 2021
1 parent 91194c2 commit 43c3907
Show file tree
Hide file tree
Showing 16 changed files with 975 additions and 39 deletions.
1 change: 1 addition & 0 deletions packages/expo-cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
"@expo/results": "^1.0.0",
"@expo/simple-spinner": "1.0.2",
"@expo/spawn-async": "1.5.0",
"@expo/xcpretty": "^1.0.2",
"@hapi/joi": "^17.1.1",
"babel-runtime": "6.26.0",
"base32.js": "0.1.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/expo-cli/src/commands/eject/updatePackageJson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Log from '../../log';
import * as CreateApp from '../utils/CreateApp';
import { isModuleSymlinked } from '../utils/isModuleSymlinked';

type DependenciesMap = { [key: string]: string | number };
export type DependenciesMap = { [key: string]: string | number };

export type DependenciesModificationResults = {
hasNewDependencies: boolean;
Expand Down Expand Up @@ -161,7 +161,7 @@ export function updatePackageJSONDependencies({
*
* @param dependencies - ideally an object of type {[key]: string} - if not then this will error.
*/
function createDependenciesMap(dependencies: any): DependenciesMap {
export function createDependenciesMap(dependencies: any): DependenciesMap {
if (typeof dependencies !== 'object') {
throw new Error(`Dependency map is invalid, expected object but got ${typeof dependencies}`);
} else if (!dependencies) {
Expand Down
14 changes: 14 additions & 0 deletions packages/expo-cli/src/commands/run/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Command } from 'commander';

import CommandError from '../../CommandError';
import buildAndroidClientAsync from './buildAndroidClientAsync';
import { runIosActionAsync } from './ios/runIos';

type Options = {
platform?: string;
Expand All @@ -24,4 +25,17 @@ export default function (program: Command) {
.description('Build a development client and run it in on a device.')
.option('--build-variant [name]', '(Android) build variant', 'release')
.asyncActionProjectDir(runAndroidAsync);
program
.command('run:ios [path]')
.description('Run the iOS app binary locally')
.helpGroup('internal')
.option('-d, --device [device]', 'Device name or UDID to build the app on')
.option('-p, --port <port>', 'Port to start the Metro bundler on. Default: 8081')
.option('--scheme <scheme>', 'Scheme to build')
.option('--bundler', 'Should start the bundler automatically')
.option(
'--configuration <configuration>',
'Xcode configuration to use. Debug or Release. Default: Debug'
)
.asyncActionProjectDir(runIosActionAsync, { checkConfig: false });
}
121 changes: 121 additions & 0 deletions packages/expo-cli/src/commands/run/ios/ExpoLogFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Formatter, Parser } from '@expo/xcpretty';
import { switchRegex } from '@expo/xcpretty/build/switchRegex';
import chalk from 'chalk';

import Log from '../../../log';

const ERROR = '❌ ';

function moduleNameFromPath(modulePath: string) {
if (modulePath.startsWith('@')) {
const [org, packageName] = modulePath.split('/');
if (org && packageName) {
return [org, packageName].join('/');
}
return modulePath;
}
const [packageName] = modulePath.split('/');
return packageName ? packageName : modulePath;
}

function getNodeModuleName(filePath: string): string | null {
// '/Users/evanbacon/Documents/GitHub/lab/yolo5/node_modules/react-native-reanimated/ios/Nodes/REACallFuncNode.m'
const [, modulePath] = filePath.split('/node_modules/');
if (modulePath) {
return moduleNameFromPath(modulePath);
}
return null;
}

class CustomParser extends Parser {
private isCollectingMetroError = false;
private metroError: string[] = [];

constructor(public formatter: ExpoLogFormatter) {
super(formatter);
}

parse(text: string): void | string {
const results = this.checkMetroError(text);
if (results) {
return results;
}
return super.parse(text);
}

// Error for the build script wrapper in expo-updates that catches metro bundler errors.
// This can be repro'd by importing a file that doesn't exist, then building.
// Metro will fail to generate the JS bundle, and throw an error that should be caught here.
checkMetroError(text: string) {
// In expo-updates, we wrap the bundler script and add regex around the error message so we can present it nicely to the user.
return switchRegex(text, [
[
/@build-script-error-begin/m,
() => {
this.isCollectingMetroError = true;
},
],
[
/@build-script-error-end/m,
() => {
const results = this.metroError.join('\n');
// Reset the metro collection error array (should never need this).
this.isCollectingMetroError = false;
this.metroError = [];
return this.formatter.formatMetroAssetCollectionError(results);
},
],
[
null,
() => {
// Collect all the lines in the metro build error
if (this.isCollectingMetroError) {
let results = text;
if (!this.metroError.length) {
const match = text.match(
/Error loading assets JSON from Metro.*steps correctly.((.|\n)*)/m
);
if (match && match[1]) {
results = match[1].trim();
}
}
this.metroError.push(results);
}
},
],
]);
}
}

export class ExpoLogFormatter extends Formatter {
constructor(props: { projectRoot: string }) {
super(props);
this.parser = new CustomParser(this);
}

formatMetroAssetCollectionError(errorContents: string): string {
const results = `\n${chalk.red(
ERROR +
// Provide proper attribution.
'Metro encountered an error:\n' +
errorContents
)}\n`;
this.errors.push(results);
return results;
}

shouldShowCompileWarning(filePath: string, lineNumber?: string, columnNumber?: string): boolean {
if (Log.isDebug) return true;
return !filePath.match(/node_modules/) && !filePath.match(/\/ios\/Pods\//);
}

formatCompile(fileName: string, filePath: string): string {
const moduleName = getNodeModuleName(filePath);
const moduleNameTag = moduleName ? chalk.dim(`(${moduleName})`) : undefined;
return ['\u203A', 'Compiling', fileName, moduleNameTag].filter(Boolean).join(' ');
}

finish() {
Log.log(`\n\u203A ${this.errors.length} error(s), and ${this.warnings.length} warning(s)\n`);
}
}
37 changes: 37 additions & 0 deletions packages/expo-cli/src/commands/run/ios/IOSDeploy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import spawnAsync from '@expo/spawn-async';
import chalk from 'chalk';
import { spawnSync } from 'child_process';
import wrapAnsi from 'wrap-ansi';

import CommandError, { SilentError } from '../../../CommandError';
import log from '../../../log';

export async function isInstalledAsync() {
try {
await spawnAsync('ios-deploy', ['--version'], { stdio: 'ignore' });
return true;
} catch {
return false;
}
}

export function installBinaryOnDevice({ bundle, udid }: { bundle: string; udid: string }) {
const iosDeployInstallArgs = ['--bundle', bundle, '--id', udid, '--justlaunch', '--debug'];

const output = spawnSync('ios-deploy', iosDeployInstallArgs, { encoding: 'utf8' });

if (output.error) {
throw new CommandError(
`Failed to install the app on device. Error in "ios-deploy" command: ${output.error.message}`
);
}
}

export async function assertInstalledAsync() {
if (!(await isInstalledAsync())) {
// Controlled error message.
const error = `Cannot install iOS apps on devices without ${chalk.bold`ios-deploy`} installed globally. Please install it with ${chalk.bold`brew install ios-deploy`} and try again, or build the app with a simulator.`;
log.warn(wrapAnsi(error, process.stdout.columns || 80));
throw new SilentError(error);
}
}
125 changes: 125 additions & 0 deletions packages/expo-cli/src/commands/run/ios/Podfile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import { getPackageJson, PackageJSONConfig } from '@expo/config';
import JsonFile from '@expo/json-file';
import chalk from 'chalk';
import fs from 'fs-extra';
import { safeLoad } from 'js-yaml';
import * as path from 'path';

import Log from '../../../log';
import { hashForDependencyMap } from '../../eject/updatePackageJson';
import { installCocoaPodsAsync } from '../../utils/CreateApp';

const CHECKSUM_KEY = 'SPEC CHECKSUMS';

export function getDependenciesFromPodfileLock(podfileLockPath: string) {
Log.debug(`Reading ${podfileLockPath}`);
let fileContent;
try {
fileContent = fs.readFileSync(podfileLockPath, 'utf8');
} catch (err) {
Log.error(
`Could not find "Podfile.lock" at ${chalk.dim(podfileLockPath)}. Did you run "${chalk.bold(
'npx pod-install'
)}"?`
);
return [];
}

// Previous portions of the lock file could be invalid yaml.
// Only parse parts that are valid
const tail = fileContent.split(CHECKSUM_KEY).slice(1);
const checksumTail = CHECKSUM_KEY + tail;

return Object.keys(safeLoad(checksumTail)[CHECKSUM_KEY] || {});
}

function getTempPrebuildFolder(projectRoot: string) {
return path.join(projectRoot, '.expo', 'prebuild');
}

type PackageChecksums = {
dependencies: string;
devDependencies: string;
};

function hasNewDependenciesSinceLastBuild(projectRoot: string, packageChecksums: PackageChecksums) {
// TODO: Maybe comparing lock files would be better...
const tempDir = getTempPrebuildFolder(projectRoot);
const tempPkgJsonPath = path.join(tempDir, 'cached-packages.json');
if (!fs.pathExistsSync(tempPkgJsonPath)) {
return true;
}
const { dependencies, devDependencies } = JsonFile.read(tempPkgJsonPath);
// Only change the dependencies if the normalized hash changes, this helps to reduce meaningless changes.
const hasNewDependencies = packageChecksums.dependencies !== dependencies;
const hasNewDevDependencies = packageChecksums.devDependencies !== devDependencies;

return hasNewDependencies || hasNewDevDependencies;
}

function createPackageChecksums(pkg: PackageJSONConfig): PackageChecksums {
return {
dependencies: hashForDependencyMap(pkg.dependencies || {}),
devDependencies: hashForDependencyMap(pkg.devDependencies || {}),
};
}

async function hasPackageJsonDependencyListChangedAsync(projectRoot: string) {
const pkg = getPackageJson(projectRoot);

const packages = createPackageChecksums(pkg);
const hasNewDependencies = hasNewDependenciesSinceLastBuild(projectRoot, packages);

// Cache package.json
const tempDir = path.join(getTempPrebuildFolder(projectRoot), 'cached-packages.json');
await fs.ensureFile(tempDir);
await JsonFile.writeAsync(tempDir, packages);

return hasNewDependencies;
}

function doesProjectUseCocoaPods(projectRoot: string): boolean {
return fs.existsSync(path.join(projectRoot, 'ios', 'Podfile'));
}

function isLockfileCreated(projectRoot: string): boolean {
const podfileLockPath = path.join(projectRoot, 'ios', 'Podfile.lock');
return fs.existsSync(podfileLockPath);
}

// TODO: Same process but with app.config changes + default plugins.
// This will ensure the user is prompted for extra setup.
export default async function maybePromptToSyncPodsAsync(projectRoot: string) {
if (!doesProjectUseCocoaPods(projectRoot)) {
// Project does not use CocoaPods
return;
}
if (!isLockfileCreated(projectRoot)) {
await installCocoaPodsAsync(projectRoot);
return;
}

// Getting autolinked packages can be heavy, optimize around checking every time.
if (!(await hasPackageJsonDependencyListChangedAsync(projectRoot))) {
return;
}

await promptToInstallPodsAsync(projectRoot, []);
}

async function promptToInstallPodsAsync(projectRoot: string, missingPods?: string[]) {
if (missingPods?.length) {
Log.log(
`Could not find the following native modules: ${missingPods
.map(pod => chalk.bold(pod))
.join(', ')}. Did you forget to run "${chalk.bold('pod install')}" ?`
);
}

try {
await installCocoaPodsAsync(projectRoot);
} catch (error) {
fs.removeSync(path.join(getTempPrebuildFolder(projectRoot), 'cached-packages.json'));
throw error;
}
}
Loading

0 comments on commit 43c3907

Please sign in to comment.