You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

392 lines
18 KiB

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getImprovedPodInstallError = exports.getPodRepoUpdateMessage = exports.getPodUpdateMessage = exports.CocoaPodsPackageManager = exports.extractMissingDependencyError = exports.CocoaPodsError = void 0;
const spawn_async_1 = __importDefault(require("@expo/spawn-async"));
const chalk_1 = __importDefault(require("chalk"));
const fs_1 = require("fs");
const os_1 = __importDefault(require("os"));
const path_1 = __importDefault(require("path"));
const PackageManager_1 = require("./PackageManager");
class CocoaPodsError extends Error {
constructor(message, code, cause) {
super(cause ? `${message}\n└─ Cause: ${cause.message}` : message);
this.code = code;
this.cause = cause;
this.name = 'CocoaPodsError';
this.isPackageManagerError = true;
}
}
exports.CocoaPodsError = CocoaPodsError;
function extractMissingDependencyError(errorOutput) {
// [!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher`
const results = errorOutput.match(/Unable to find a specification for ['"`]([\w-_\d\s]+)['"`] depended upon by ['"`]([\w-_\d\s]+)['"`]/);
if (results) {
return [results[1], results[2]];
}
return null;
}
exports.extractMissingDependencyError = extractMissingDependencyError;
class CocoaPodsPackageManager {
constructor({ cwd, silent }) {
this.silent = !!silent;
this.options = {
cwd,
// We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
// Later we'll also pipe the stdout/stderr to the terminal when silent is false.
stdio: 'pipe',
};
}
static getPodProjectRoot(projectRoot) {
if (CocoaPodsPackageManager.isUsingPods(projectRoot))
return projectRoot;
const iosProject = path_1.default.join(projectRoot, 'ios');
if (CocoaPodsPackageManager.isUsingPods(iosProject))
return iosProject;
const macOsProject = path_1.default.join(projectRoot, 'macos');
if (CocoaPodsPackageManager.isUsingPods(macOsProject))
return macOsProject;
return null;
}
static isUsingPods(projectRoot) {
return (0, fs_1.existsSync)(path_1.default.join(projectRoot, 'Podfile'));
}
static async gemInstallCLIAsync(nonInteractive = false, spawnOptions = { stdio: 'inherit' }) {
const options = ['install', 'cocoapods', '--no-document'];
try {
// In case the user has run sudo before running the command we can properly install CocoaPods without prompting for an interaction.
await (0, spawn_async_1.default)('gem', options, spawnOptions);
}
catch (error) {
if (nonInteractive) {
throw new CocoaPodsError('Failed to install CocoaPods CLI with gem (recommended)', 'COMMAND_FAILED', error);
}
// If the user doesn't have permission then we can prompt them to use sudo.
await (0, PackageManager_1.spawnSudoAsync)(['gem', ...options], spawnOptions);
}
}
static async brewLinkCLIAsync(spawnOptions = { stdio: 'inherit' }) {
await (0, spawn_async_1.default)('brew', ['link', 'cocoapods'], spawnOptions);
}
static async brewInstallCLIAsync(spawnOptions = { stdio: 'inherit' }) {
await (0, spawn_async_1.default)('brew', ['install', 'cocoapods'], spawnOptions);
}
static async installCLIAsync({ nonInteractive = false, spawnOptions = { stdio: 'inherit' }, }) {
var _a;
if (!spawnOptions) {
spawnOptions = { stdio: 'inherit' };
}
const silent = !!spawnOptions.ignoreStdio;
try {
!silent && console.log(`\u203A Attempting to install CocoaPods CLI with Gem`);
await CocoaPodsPackageManager.gemInstallCLIAsync(nonInteractive, spawnOptions);
!silent && console.log(`\u203A Successfully installed CocoaPods CLI with Gem`);
return true;
}
catch (error) {
if (!silent) {
console.log(chalk_1.default.yellow(`\u203A Failed to install CocoaPods CLI with Gem`));
console.log(chalk_1.default.red((_a = error.stderr) !== null && _a !== void 0 ? _a : error.message));
console.log(`\u203A Attempting to install CocoaPods CLI with Homebrew`);
}
try {
await CocoaPodsPackageManager.brewInstallCLIAsync(spawnOptions);
if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
try {
await CocoaPodsPackageManager.brewLinkCLIAsync(spawnOptions);
// Still not available after linking? Bail out
if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) {
throw new CocoaPodsError('CLI could not be installed automatically with gem or Homebrew, please install CocoaPods manually and try again', 'NO_CLI', error);
}
}
catch (error) {
throw new CocoaPodsError('Homebrew installation appeared to succeed but CocoaPods CLI not found in PATH and unable to link.', 'NO_CLI', error);
}
}
!silent && console.log(`\u203A Successfully installed CocoaPods CLI with Homebrew`);
return true;
}
catch (error) {
!silent &&
console.warn(chalk_1.default.yellow(`\u203A Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`));
throw new CocoaPodsError(`Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`, 'NO_CLI', error);
}
}
}
static isAvailable(projectRoot, silent) {
if (process.platform !== 'darwin') {
!silent && console.log(chalk_1.default.red('CocoaPods is only supported on macOS machines'));
return false;
}
if (!CocoaPodsPackageManager.isUsingPods(projectRoot)) {
!silent && console.log(chalk_1.default.yellow('CocoaPods is not supported in this project'));
return false;
}
return true;
}
static async isCLIInstalledAsync(spawnOptions = { stdio: 'inherit' }) {
try {
await (0, spawn_async_1.default)('pod', ['--version'], spawnOptions);
return true;
}
catch {
return false;
}
}
get name() {
return 'CocoaPods';
}
/** Runs `pod install` and attempts to automatically run known troubleshooting steps automatically. */
async installAsync({ spinner } = {}) {
await this._installAsync({ spinner });
}
isCLIInstalledAsync() {
return CocoaPodsPackageManager.isCLIInstalledAsync(this.options);
}
installCLIAsync() {
return CocoaPodsPackageManager.installCLIAsync({
nonInteractive: true,
spawnOptions: this.options,
});
}
async handleInstallErrorAsync({ error, shouldUpdate = true, updatedPackages = [], spinner, }) {
// Unknown errors are rethrown.
if (!error.output) {
throw error;
}
// To emulate a `pod install --repo-update` error, enter your `ios/Podfile.lock` and change one of `PODS` version numbers to some lower value.
// const isPodRepoUpdateError = shouldPodRepoUpdate(output);
if (!shouldUpdate) {
// If we can't automatically fix the error, we'll just rethrow it with some known troubleshooting info.
throw getImprovedPodInstallError(error, {
cwd: this.options.cwd,
});
}
// Collect all of the spawn info.
const errorOutput = error.output.join(os_1.default.EOL).trim();
// Extract useful information from the error message and push it to the spinner.
const { updatePackage, shouldUpdateRepo } = getPodUpdateMessage(errorOutput);
if (!updatePackage || updatedPackages.includes(updatePackage)) {
// `pod install --repo-update`...
// Attempt to install again but this time with install --repo-update enabled.
return await this._installAsync({
spinner,
shouldRepoUpdate: true,
// Include a boolean to ensure pod install --repo-update isn't invoked in the unlikely case where the pods fail to update.
shouldUpdate: false,
updatedPackages,
});
}
// Store the package we should update to prevent a loop.
updatedPackages.push(updatePackage);
// If a single package is broken, we'll try to update it.
// You can manually test this by changing a version number in your `Podfile.lock`.
// Attempt `pod update <package> <--no-repo-update>` and then try again.
return await this.runInstallTypeCommandAsync(['update', updatePackage, shouldUpdateRepo ? '' : '--no-repo-update'].filter(Boolean), {
formatWarning() {
const updateMessage = `Failed to update ${chalk_1.default.bold(updatePackage)}. Attempting to update the repo instead.`;
return updateMessage;
},
spinner,
updatedPackages,
});
// // If update succeeds, we'll try to install again (skipping `pod install --repo-update`).
// return await this._installAsync({
// spinner,
// shouldUpdate: false,
// updatedPackages,
// });
}
async _installAsync({ shouldRepoUpdate, ...props } = {}) {
return await this.runInstallTypeCommandAsync(['install', shouldRepoUpdate ? '--repo-update' : ''].filter(Boolean), {
formatWarning(error) {
// Extract useful information from the error message and push it to the spinner.
return getPodRepoUpdateMessage(error.output.join(os_1.default.EOL).trim()).message;
},
...props,
});
}
async runInstallTypeCommandAsync(command, { formatWarning, ...props } = {}) {
try {
return await this._runAsync(command);
}
catch (error) {
if (formatWarning) {
const warning = formatWarning(error);
if (props.spinner) {
props.spinner.text = chalk_1.default.bold(warning);
}
if (!this.silent) {
console.warn(chalk_1.default.yellow(warning));
}
}
return await this.handleInstallErrorAsync({ error, ...props });
}
}
async addWithParametersAsync(names, parameters) {
throw new Error('Unimplemented');
}
async addAsync(...names) {
throw new Error('Unimplemented');
}
async addDevAsync(...names) {
throw new Error('Unimplemented');
}
async versionAsync() {
const { stdout } = await (0, spawn_async_1.default)('pod', ['--version'], this.options);
return stdout.trim();
}
async getConfigAsync(key) {
throw new Error('Unimplemented');
}
async removeLockfileAsync() {
throw new Error('Unimplemented');
}
async cleanAsync() {
throw new Error('Unimplemented');
}
// Private
async podRepoUpdateAsync() {
var _a;
try {
await this._runAsync(['repo', 'update']);
}
catch (error) {
error.message = error.message || ((_a = error.stderr) !== null && _a !== void 0 ? _a : error.stdout);
throw new CocoaPodsError('The command `pod install --repo-update` failed', 'COMMAND_FAILED', error);
}
}
// Exposed for testing
async _runAsync(args) {
if (!this.silent) {
console.log(`> pod ${args.join(' ')}`);
}
const promise = (0, spawn_async_1.default)('pod', [
...args,
// Enables colors while collecting output.
'--ansi',
], {
// Add the cwd and other options to the spawn options.
...this.options,
// We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors.
// This is particularly required for the `pod install --repo-update` error.
// Later we'll also pipe the stdout/stderr to the terminal when silent is false,
// currently this means we lose out on the ansi colors unless passing the `--ansi` flag to every command.
stdio: 'pipe',
});
if (!this.silent) {
// If not silent, pipe the stdout/stderr to the terminal.
// We only do this when the `stdio` is set to `pipe` (collect the results for parsing), `inherit` won't contain `promise.child`.
if (promise.child.stdout) {
promise.child.stdout.pipe(process.stdout);
}
}
return await promise;
}
}
exports.CocoaPodsPackageManager = CocoaPodsPackageManager;
/** When pods are outdated, they'll throw an error informing you to run "pod install --repo-update" */
function shouldPodRepoUpdate(errorOutput) {
const output = errorOutput;
const isPodRepoUpdateError = output.includes('pod repo update') || output.includes('--no-repo-update');
return isPodRepoUpdateError;
}
function getPodUpdateMessage(output) {
var _a;
const props = output.match(/run ['"`]pod update ([\w-_\d/]+)( --no-repo-update)?['"`] to apply changes/);
return {
updatePackage: (_a = props === null || props === void 0 ? void 0 : props[1]) !== null && _a !== void 0 ? _a : null,
shouldUpdateRepo: !(props === null || props === void 0 ? void 0 : props[2]),
};
}
exports.getPodUpdateMessage = getPodUpdateMessage;
function getPodRepoUpdateMessage(errorOutput) {
const warningInfo = extractMissingDependencyError(errorOutput);
const brokenPackage = getPodUpdateMessage(errorOutput);
let message;
if (warningInfo) {
message = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}.`;
}
else if (brokenPackage === null || brokenPackage === void 0 ? void 0 : brokenPackage.updatePackage) {
message = `Couldn't install: ${brokenPackage === null || brokenPackage === void 0 ? void 0 : brokenPackage.updatePackage}.`;
}
else {
message = `Couldn't install Pods.`;
}
message += ` Updating the Pods project and trying again...`;
return { message, ...brokenPackage };
}
exports.getPodRepoUpdateMessage = getPodRepoUpdateMessage;
/**
* Format the CocoaPods CLI install error.
*
* @param error Error from CocoaPods CLI `pod install` command.
* @returns
*/
function getImprovedPodInstallError(error, { cwd = process.cwd() }) {
var _a, _b;
// Collect all of the spawn info.
const errorOutput = error.output.join(os_1.default.EOL).trim();
if (error.stdout.match(/No [`'"]Podfile[`'"] found in the project directory/)) {
// Ran pod install but no Podfile was found.
error.message = `No Podfile found in directory: ${cwd}. Ensure CocoaPods is setup any try again.`;
}
else if (shouldPodRepoUpdate(errorOutput)) {
// Ran pod install but the install --repo-update step failed.
const warningInfo = extractMissingDependencyError(errorOutput);
let reason;
if (warningInfo) {
reason = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}`;
}
else {
reason = `This is often due to native package versions mismatching`;
}
// Attempt to provide a helpful message about the missing NPM dependency (containing a CocoaPod) since React Native
// developers will almost always be using autolinking and not interacting with CocoaPods directly.
let solution;
if (warningInfo === null || warningInfo === void 0 ? void 0 : warningInfo[0]) {
// If the missing package is named `expo-dev-menu`, `react-native`, etc. then it might not be installed in the project.
if (warningInfo[0].match(/^(?:@?expo|@?react)(-|\/)/)) {
solution = `Ensure the node module "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
}
else {
solution = `Ensure the CocoaPod "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`;
}
}
else {
// Brute force
solution = `Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.`;
}
error.message = `${reason}. ${solution}`;
// Attempt to provide the troubleshooting info from CocoaPods CLI at the bottom of the error message.
if (error.stdout) {
const cocoapodsDebugInfo = error.stdout.split(os_1.default.EOL);
// The troubleshooting info starts with `[!]`, capture everything after that.
const firstWarning = cocoapodsDebugInfo.findIndex(v => v.startsWith('[!]'));
if (firstWarning !== -1) {
const warning = cocoapodsDebugInfo.slice(firstWarning).join(os_1.default.EOL);
error.message += `\n\n${chalk_1.default.gray(warning)}`;
}
}
return new CocoaPodsError('Command `pod install --repo-update` failed.', 'COMMAND_FAILED', error);
}
else {
let stderr = error.stderr.trim();
// CocoaPods CLI prints the useful error to stdout...
const usefulError = (_a = error.stdout.match(/\[!\]\s((?:.|\n)*)/)) === null || _a === void 0 ? void 0 : _a[1];
// If there is a useful error message then prune the less useful info.
if (usefulError) {
// Delete unhelpful CocoaPods CLI error message.
if ((_b = error.message) === null || _b === void 0 ? void 0 : _b.match(/pod exited with non-zero code: 1/)) {
error.message = '';
}
stderr = null;
}
error.message = [usefulError, error.message, stderr].filter(Boolean).join('\n');
}
return new CocoaPodsError('Command `pod install` failed.', 'COMMAND_FAILED', error);
}
exports.getImprovedPodInstallError = getImprovedPodInstallError;
//# sourceMappingURL=CocoaPodsPackageManager.js.map