1"use strict"; 2var __importDefault = (this && this.__importDefault) || function (mod) { 3 return (mod && mod.__esModule) ? mod : { "default": mod }; 4}; 5Object.defineProperty(exports, "__esModule", { value: true }); 6exports.getImprovedPodInstallError = exports.getPodRepoUpdateMessage = exports.getPodUpdateMessage = exports.CocoaPodsPackageManager = exports.extractMissingDependencyError = exports.CocoaPodsError = void 0; 7const spawn_async_1 = __importDefault(require("@expo/spawn-async")); 8const chalk_1 = __importDefault(require("chalk")); 9const fs_1 = require("fs"); 10const os_1 = __importDefault(require("os")); 11const path_1 = __importDefault(require("path")); 12const spawn_1 = require("../utils/spawn"); 13class CocoaPodsError extends Error { 14 code; 15 cause; 16 name = 'CocoaPodsError'; 17 isPackageManagerError = true; 18 constructor(message, code, cause) { 19 super(cause ? `${message}\n└─ Cause: ${cause.message}` : message); 20 this.code = code; 21 this.cause = cause; 22 } 23} 24exports.CocoaPodsError = CocoaPodsError; 25function extractMissingDependencyError(errorOutput) { 26 // [!] Unable to find a specification for `expo-dev-menu-interface` depended upon by `expo-dev-launcher` 27 const results = errorOutput.match(/Unable to find a specification for ['"`]([\w-_\d\s]+)['"`] depended upon by ['"`]([\w-_\d\s]+)['"`]/); 28 if (results) { 29 return [results[1], results[2]]; 30 } 31 return null; 32} 33exports.extractMissingDependencyError = extractMissingDependencyError; 34class CocoaPodsPackageManager { 35 options; 36 silent; 37 static getPodProjectRoot(projectRoot) { 38 if (CocoaPodsPackageManager.isUsingPods(projectRoot)) 39 return projectRoot; 40 const iosProject = path_1.default.join(projectRoot, 'ios'); 41 if (CocoaPodsPackageManager.isUsingPods(iosProject)) 42 return iosProject; 43 const macOsProject = path_1.default.join(projectRoot, 'macos'); 44 if (CocoaPodsPackageManager.isUsingPods(macOsProject)) 45 return macOsProject; 46 return null; 47 } 48 static isUsingPods(projectRoot) { 49 return (0, fs_1.existsSync)(path_1.default.join(projectRoot, 'Podfile')); 50 } 51 static async gemInstallCLIAsync(nonInteractive = false, spawnOptions = { stdio: 'inherit' }) { 52 const options = ['install', 'cocoapods', '--no-document']; 53 try { 54 // In case the user has run sudo before running the command we can properly install CocoaPods without prompting for an interaction. 55 await (0, spawn_async_1.default)('gem', options, spawnOptions); 56 } 57 catch (error) { 58 if (nonInteractive) { 59 throw new CocoaPodsError('Failed to install CocoaPods CLI with gem (recommended)', 'COMMAND_FAILED', error); 60 } 61 // If the user doesn't have permission then we can prompt them to use sudo. 62 await (0, spawn_1.spawnSudoAsync)(['gem', ...options], spawnOptions); 63 } 64 } 65 static async brewLinkCLIAsync(spawnOptions = { stdio: 'inherit' }) { 66 await (0, spawn_async_1.default)('brew', ['link', 'cocoapods'], spawnOptions); 67 } 68 static async brewInstallCLIAsync(spawnOptions = { stdio: 'inherit' }) { 69 await (0, spawn_async_1.default)('brew', ['install', 'cocoapods'], spawnOptions); 70 } 71 static async installCLIAsync({ nonInteractive = false, spawnOptions = { stdio: 'inherit' }, }) { 72 if (!spawnOptions) { 73 spawnOptions = { stdio: 'inherit' }; 74 } 75 const silent = !!spawnOptions.ignoreStdio; 76 try { 77 !silent && console.log(`\u203A Attempting to install CocoaPods CLI with Gem`); 78 await CocoaPodsPackageManager.gemInstallCLIAsync(nonInteractive, spawnOptions); 79 !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Gem`); 80 return true; 81 } 82 catch (error) { 83 if (!silent) { 84 console.log(chalk_1.default.yellow(`\u203A Failed to install CocoaPods CLI with Gem`)); 85 console.log(chalk_1.default.red(error.stderr ?? error.message)); 86 console.log(`\u203A Attempting to install CocoaPods CLI with Homebrew`); 87 } 88 try { 89 await CocoaPodsPackageManager.brewInstallCLIAsync(spawnOptions); 90 if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) { 91 try { 92 await CocoaPodsPackageManager.brewLinkCLIAsync(spawnOptions); 93 // Still not available after linking? Bail out 94 if (!(await CocoaPodsPackageManager.isCLIInstalledAsync(spawnOptions))) { 95 throw new CocoaPodsError('CLI could not be installed automatically with gem or Homebrew, please install CocoaPods manually and try again', 'NO_CLI', error); 96 } 97 } 98 catch (error) { 99 throw new CocoaPodsError('Homebrew installation appeared to succeed but CocoaPods CLI not found in PATH and unable to link.', 'NO_CLI', error); 100 } 101 } 102 !silent && console.log(`\u203A Successfully installed CocoaPods CLI with Homebrew`); 103 return true; 104 } 105 catch (error) { 106 !silent && 107 console.warn(chalk_1.default.yellow(`\u203A Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`)); 108 throw new CocoaPodsError(`Failed to install CocoaPods with Homebrew. Please install CocoaPods CLI manually and try again.`, 'NO_CLI', error); 109 } 110 } 111 } 112 static isAvailable(projectRoot, silent) { 113 if (process.platform !== 'darwin') { 114 !silent && console.log(chalk_1.default.red('CocoaPods is only supported on macOS machines')); 115 return false; 116 } 117 if (!CocoaPodsPackageManager.isUsingPods(projectRoot)) { 118 !silent && console.log(chalk_1.default.yellow('CocoaPods is not supported in this project')); 119 return false; 120 } 121 return true; 122 } 123 static async isCLIInstalledAsync(spawnOptions = { stdio: 'inherit' }) { 124 try { 125 await (0, spawn_async_1.default)('pod', ['--version'], spawnOptions); 126 return true; 127 } 128 catch { 129 return false; 130 } 131 } 132 constructor({ cwd, silent }) { 133 this.silent = !!silent; 134 this.options = { 135 cwd, 136 // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors. 137 // Later we'll also pipe the stdout/stderr to the terminal when silent is false. 138 stdio: 'pipe', 139 }; 140 } 141 get name() { 142 return 'CocoaPods'; 143 } 144 /** Runs `pod install` and attempts to automatically run known troubleshooting steps automatically. */ 145 async installAsync({ spinner } = {}) { 146 await this._installAsync({ spinner }); 147 } 148 isCLIInstalledAsync() { 149 return CocoaPodsPackageManager.isCLIInstalledAsync(this.options); 150 } 151 installCLIAsync() { 152 return CocoaPodsPackageManager.installCLIAsync({ 153 nonInteractive: true, 154 spawnOptions: this.options, 155 }); 156 } 157 async handleInstallErrorAsync({ error, shouldUpdate = true, updatedPackages = [], spinner, }) { 158 // Unknown errors are rethrown. 159 if (!error.output) { 160 throw error; 161 } 162 // To emulate a `pod install --repo-update` error, enter your `ios/Podfile.lock` and change one of `PODS` version numbers to some lower value. 163 // const isPodRepoUpdateError = shouldPodRepoUpdate(output); 164 if (!shouldUpdate) { 165 // If we can't automatically fix the error, we'll just rethrow it with some known troubleshooting info. 166 throw getImprovedPodInstallError(error, { 167 cwd: this.options.cwd, 168 }); 169 } 170 // Collect all of the spawn info. 171 const errorOutput = error.output.join(os_1.default.EOL).trim(); 172 // Extract useful information from the error message and push it to the spinner. 173 const { updatePackage, shouldUpdateRepo } = getPodUpdateMessage(errorOutput); 174 if (!updatePackage || updatedPackages.includes(updatePackage)) { 175 // `pod install --repo-update`... 176 // Attempt to install again but this time with install --repo-update enabled. 177 return await this._installAsync({ 178 spinner, 179 shouldRepoUpdate: true, 180 // Include a boolean to ensure pod install --repo-update isn't invoked in the unlikely case where the pods fail to update. 181 shouldUpdate: false, 182 updatedPackages, 183 }); 184 } 185 // Store the package we should update to prevent a loop. 186 updatedPackages.push(updatePackage); 187 // If a single package is broken, we'll try to update it. 188 // You can manually test this by changing a version number in your `Podfile.lock`. 189 // Attempt `pod update <package> <--no-repo-update>` and then try again. 190 return await this.runInstallTypeCommandAsync(['update', updatePackage, shouldUpdateRepo ? '' : '--no-repo-update'].filter(Boolean), { 191 formatWarning() { 192 const updateMessage = `Failed to update ${chalk_1.default.bold(updatePackage)}. Attempting to update the repo instead.`; 193 return updateMessage; 194 }, 195 spinner, 196 updatedPackages, 197 }); 198 // // If update succeeds, we'll try to install again (skipping `pod install --repo-update`). 199 // return await this._installAsync({ 200 // spinner, 201 // shouldUpdate: false, 202 // updatedPackages, 203 // }); 204 } 205 async _installAsync({ shouldRepoUpdate, ...props } = {}) { 206 return await this.runInstallTypeCommandAsync(['install', shouldRepoUpdate ? '--repo-update' : ''].filter(Boolean), { 207 formatWarning(error) { 208 // Extract useful information from the error message and push it to the spinner. 209 return getPodRepoUpdateMessage(error.output.join(os_1.default.EOL).trim()).message; 210 }, 211 ...props, 212 }); 213 } 214 async runInstallTypeCommandAsync(command, { formatWarning, ...props } = {}) { 215 try { 216 return await this._runAsync(command); 217 } 218 catch (error) { 219 if (formatWarning) { 220 const warning = formatWarning(error); 221 if (props.spinner) { 222 props.spinner.text = chalk_1.default.bold(warning); 223 } 224 if (!this.silent) { 225 console.warn(chalk_1.default.yellow(warning)); 226 } 227 } 228 return await this.handleInstallErrorAsync({ error, ...props }); 229 } 230 } 231 async addWithParametersAsync(names, parameters) { 232 throw new Error('Unimplemented'); 233 } 234 addAsync(names = []) { 235 throw new Error('Unimplemented'); 236 } 237 addDevAsync(names = []) { 238 throw new Error('Unimplemented'); 239 } 240 addGlobalAsync(names = []) { 241 throw new Error('Unimplemented'); 242 } 243 removeAsync(names = []) { 244 throw new Error('Unimplemented'); 245 } 246 removeDevAsync(names = []) { 247 throw new Error('Unimplemented'); 248 } 249 removeGlobalAsync(names = []) { 250 throw new Error('Unimplemented'); 251 } 252 async versionAsync() { 253 const { stdout } = await (0, spawn_async_1.default)('pod', ['--version'], this.options); 254 return stdout.trim(); 255 } 256 async configAsync(key) { 257 throw new Error('Unimplemented'); 258 } 259 async removeLockfileAsync() { 260 throw new Error('Unimplemented'); 261 } 262 async uninstallAsync() { 263 throw new Error('Unimplemented'); 264 } 265 // Private 266 async podRepoUpdateAsync() { 267 try { 268 await this._runAsync(['repo', 'update']); 269 } 270 catch (error) { 271 error.message = error.message || (error.stderr ?? error.stdout); 272 throw new CocoaPodsError('The command `pod install --repo-update` failed', 'COMMAND_FAILED', error); 273 } 274 } 275 // Exposed for testing 276 async _runAsync(args) { 277 if (!this.silent) { 278 console.log(`> pod ${args.join(' ')}`); 279 } 280 const promise = (0, spawn_async_1.default)('pod', [ 281 ...args, 282 // Enables colors while collecting output. 283 '--ansi', 284 ], { 285 // Add the cwd and other options to the spawn options. 286 ...this.options, 287 // We use pipe by default instead of inherit so that we can capture stderr/stdout and process it for errors. 288 // This is particularly required for the `pod install --repo-update` error. 289 // Later we'll also pipe the stdout/stderr to the terminal when silent is false, 290 // currently this means we lose out on the ansi colors unless passing the `--ansi` flag to every command. 291 stdio: 'pipe', 292 }); 293 if (!this.silent) { 294 // If not silent, pipe the stdout/stderr to the terminal. 295 // We only do this when the `stdio` is set to `pipe` (collect the results for parsing), `inherit` won't contain `promise.child`. 296 if (promise.child.stdout) { 297 promise.child.stdout.pipe(process.stdout); 298 } 299 } 300 return await promise; 301 } 302} 303exports.CocoaPodsPackageManager = CocoaPodsPackageManager; 304/** When pods are outdated, they'll throw an error informing you to run "pod install --repo-update" */ 305function shouldPodRepoUpdate(errorOutput) { 306 const output = errorOutput; 307 const isPodRepoUpdateError = output.includes('pod repo update') || output.includes('--no-repo-update'); 308 return isPodRepoUpdateError; 309} 310function getPodUpdateMessage(output) { 311 const props = output.match(/run ['"`]pod update ([\w-_\d/]+)( --no-repo-update)?['"`] to apply changes/); 312 return { 313 updatePackage: props?.[1] ?? null, 314 shouldUpdateRepo: !props?.[2], 315 }; 316} 317exports.getPodUpdateMessage = getPodUpdateMessage; 318function getPodRepoUpdateMessage(errorOutput) { 319 const warningInfo = extractMissingDependencyError(errorOutput); 320 const brokenPackage = getPodUpdateMessage(errorOutput); 321 let message; 322 if (warningInfo) { 323 message = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}.`; 324 } 325 else if (brokenPackage?.updatePackage) { 326 message = `Couldn't install: ${brokenPackage?.updatePackage}.`; 327 } 328 else { 329 message = `Couldn't install Pods.`; 330 } 331 message += ` Updating the Pods project and trying again...`; 332 return { message, ...brokenPackage }; 333} 334exports.getPodRepoUpdateMessage = getPodRepoUpdateMessage; 335/** 336 * Format the CocoaPods CLI install error. 337 * 338 * @param error Error from CocoaPods CLI `pod install` command. 339 * @returns 340 */ 341function getImprovedPodInstallError(error, { cwd = process.cwd() }) { 342 // Collect all of the spawn info. 343 const errorOutput = error.output.join(os_1.default.EOL).trim(); 344 if (error.stdout.match(/No [`'"]Podfile[`'"] found in the project directory/)) { 345 // Ran pod install but no Podfile was found. 346 error.message = `No Podfile found in directory: ${cwd}. Ensure CocoaPods is setup any try again.`; 347 } 348 else if (shouldPodRepoUpdate(errorOutput)) { 349 // Ran pod install but the install --repo-update step failed. 350 const warningInfo = extractMissingDependencyError(errorOutput); 351 let reason; 352 if (warningInfo) { 353 reason = `Couldn't install: ${warningInfo[1]} » ${chalk_1.default.underline(warningInfo[0])}`; 354 } 355 else { 356 reason = `This is often due to native package versions mismatching`; 357 } 358 // Attempt to provide a helpful message about the missing NPM dependency (containing a CocoaPod) since React Native 359 // developers will almost always be using autolinking and not interacting with CocoaPods directly. 360 let solution; 361 if (warningInfo?.[0]) { 362 // If the missing package is named `expo-dev-menu`, `react-native`, etc. then it might not be installed in the project. 363 if (warningInfo[0].match(/^(?:@?expo|@?react)(-|\/)/)) { 364 solution = `Ensure the node module "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`; 365 } 366 else { 367 solution = `Ensure the CocoaPod "${warningInfo[0]}" is installed in your project, then run 'npx pod-install' to try again.`; 368 } 369 } 370 else { 371 // Brute force 372 solution = `Try deleting the 'ios/Pods' folder or the 'ios/Podfile.lock' file and running 'npx pod-install' to resolve.`; 373 } 374 error.message = `${reason}. ${solution}`; 375 // Attempt to provide the troubleshooting info from CocoaPods CLI at the bottom of the error message. 376 if (error.stdout) { 377 const cocoapodsDebugInfo = error.stdout.split(os_1.default.EOL); 378 // The troubleshooting info starts with `[!]`, capture everything after that. 379 const firstWarning = cocoapodsDebugInfo.findIndex((v) => v.startsWith('[!]')); 380 if (firstWarning !== -1) { 381 const warning = cocoapodsDebugInfo.slice(firstWarning).join(os_1.default.EOL); 382 error.message += `\n\n${chalk_1.default.gray(warning)}`; 383 } 384 } 385 return new CocoaPodsError('Command `pod install --repo-update` failed.', 'COMMAND_FAILED', error); 386 } 387 else { 388 let stderr = error.stderr.trim(); 389 // CocoaPods CLI prints the useful error to stdout... 390 const usefulError = error.stdout.match(/\[!\]\s((?:.|\n)*)/)?.[1]; 391 // If there is a useful error message then prune the less useful info. 392 if (usefulError) { 393 // Delete unhelpful CocoaPods CLI error message. 394 if (error.message?.match(/pod exited with non-zero code: 1/)) { 395 error.message = ''; 396 } 397 stderr = null; 398 } 399 error.message = [usefulError, error.message, stderr].filter(Boolean).join('\n'); 400 } 401 return new CocoaPodsError('Command `pod install` failed.', 'COMMAND_FAILED', error); 402} 403exports.getImprovedPodInstallError = getImprovedPodInstallError; 404//# sourceMappingURL=CocoaPodsPackageManager.js.map