Skip to content

Instantly share code, notes, and snippets.

@masonwolters
Last active February 18, 2025 22:14
Show Gist options
  • Save masonwolters/927c926d6adec5e9ca9762e4d38f48c5 to your computer and use it in GitHub Desktop.
Save masonwolters/927c926d6adec5e9ca9762e4d38f48c5 to your computer and use it in GitHub Desktop.
React native iOS with pre-built binaries
#
# This script is run in CI on master commits. It builds a debug binary
# for the iOS simulator and uploads it to the mobile-binaries repo.
#
# Then, when developer run `yarn ios` to develop locally, instead of
# having to build all of the native code, we just download the pre-built
# binary and install it on the simulator.
#
REPO_URL="{YOUR_GIT_URL}"
REPO_FOLDER="dev-binaries"
BINARY_NAME="My App.app" # replace
BINARY_PATH="/var/tmp/Debug-iphonesimulator/$BINARY_NAME"
BUILD_NUM=$(git rev-list --count HEAD)
GIT_COMMIT_HASH=$(git rev-parse HEAD)
# exit when any command fails
set -e
# build debug binary for simulator
bundle exec fastlane build_simulator_debug
rm -rf $REPO_FOLDER
mkdir $REPO_FOLDER
# zip binary
mv "$BINARY_PATH" .
zip -r $REPO_FOLDER/ios.zip "$BINARY_NAME"
rm -rf "$BINARY_NAME"
cd $REPO_FOLDER
git init
git remote add origin $REPO_URL
git add .
git commit -m "Build #$BUILD_NUM, for master commit $GIT_COMMIT_HASH"
# wipe out all git history because the binaries are big
git push origin master -f
echo "Successfully built iOS simulator binary and uploaded to GitHub!"
/*
This should be invoked from package.json like:
"scripts": {
"ios": "babel-node ios.js",
}
*/
import chalk from 'chalk';
import fs from 'fs';
const { program } = require('commander');
const { execSync, exec: execAsync, spawn } = require('child_process');
const { getIsPackagerRunning } = require('./utils/packager');
const BINARIES_REPO_URL = '{YOUR_GIT_URL}';
const BINARIES_FOLDER = 'dev-binaries';
const SIMULATOR_PATH = '/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app';
const BUNDLE_ID = '{YOUR_APP_BUNDLE_IDENTIFIER';
const BINARY_NAME = `My\\ App.app`; // replace
const PACKAGER_TIMEOUT_SECONDS = 1; // Give packager time to start before launching app
const exec = (command) => {
const result = execSync(command);
if (result) {
return result.toString();
}
return result;
};
/**
* Download and unzip pre-built binaries from github repo.
*/
const pullBinariesRepo = () => {
console.log('Downloading pre-built binaries from GitHub...');
const folderExists = fs.existsSync(BINARIES_FOLDER);
if (folderExists) {
exec(`rm -rf ${BINARIES_FOLDER}`);
}
exec(`git clone ${BINARIES_REPO_URL} ${BINARIES_FOLDER}`);
exec(`cd ${BINARIES_FOLDER} && unzip -o ios.zip && cd ..`);
console.log(chalk.green('Done downloading and unzipping pre-built binaries'));
};
/**
* Find the UUID of the simulator we want by using xcrun simctl list and
* searching through the output.
*/
const findSimulatorId = (name, ipad) => {
const output = exec('xcrun simctl list');
const [, devices] = output.split('== Devices ==');
if (name) {
const match = devices.match(new RegExp(`${name}.+\\((.+?)\\) \\(.+?\\) $`, 'm'));
const id = match && match[1];
if (!id) {
throw new Error(`No simulator found with name ${name}`);
}
return id;
} else if (ipad) {
// Find last iPad
const matches = [...devices.matchAll(/iPad.+Air.+ \((.+?)\) \(.+?\) $/gm)];
return matches[matches.length - 1][1];
} else {
// Find last iPhone
const matches = [...devices.matchAll(/iPhone.+Pro \((.+?)\) \(.+?\) $/gm)];
return matches[matches.length - 1][1];
}
};
const bootSimulator = (simulatorId) => {
console.log('Booting simulator...');
try {
exec(`xcrun simctl boot ${simulatorId}`);
} catch (e) {
// this will throw an error if the simulator is already booted, so we just ignore it
}
exec(`open ${SIMULATOR_PATH}`);
console.log(chalk.green('Done booting iOS simulator'));
};
const installApp = (simulatorId) => {
console.log('Installing app on simulator...');
exec(`xcrun simctl install ${simulatorId} ${BINARIES_FOLDER}/${BINARY_NAME}`);
console.log(chalk.green('Done installing app on simulator'));
};
const launchApp = (simulatorId, afterSeconds) => {
execAsync(`(sleep ${afterSeconds}; xcrun simctl launch ${simulatorId} ${BUNDLE_ID}) &`);
};
/**
* Start metro bundler for JS bundle.
*/
const startPackager = () => {
console.log(`Packager is not running yet, starting it...`);
spawn('yarn', ['start'], { stdio: 'inherit' });
};
const main = () => {
const options = program.opts();
pullBinariesRepo();
const simulatorId = findSimulatorId(options.simulator, options.ipad);
bootSimulator(simulatorId);
installApp(simulatorId);
const isPackagerRunning = getIsPackagerRunning();
launchApp(simulatorId, isPackagerRunning ? 0 : PACKAGER_TIMEOUT_SECONDS);
if (!isPackagerRunning) {
startPackager();
}
};
program
.option('--ipad', 'Run on iPad simulator (defaults to iPhone)')
.option('-s, --simulator <name>', 'Simulator name to run');
program.parse();
main();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment