18d307f52SEvan Baconimport { getPackageJson, PackageJSONConfig } from '@expo/config'; 28d307f52SEvan Baconimport JsonFile from '@expo/json-file'; 38d307f52SEvan Baconimport * as PackageManager from '@expo/package-manager'; 48d307f52SEvan Baconimport chalk from 'chalk'; 58d307f52SEvan Baconimport fs from 'fs'; 68d307f52SEvan Baconimport path from 'path'; 78d307f52SEvan Bacon 88d307f52SEvan Baconimport { ensureDirectoryAsync } from './dir'; 9814b6fafSEvan Baconimport { env } from './env'; 10c4ef02aeSEvan Baconimport { AbortCommandError } from './errors'; 118d307f52SEvan Baconimport { logNewSection } from './ora'; 128a424bebSJames Ideimport * as Log from '../log'; 138a424bebSJames Ideimport { hashForDependencyMap } from '../prebuild/updatePackageJson'; 148d307f52SEvan Bacon 158d307f52SEvan Bacontype PackageChecksums = { 164c50faceSEvan Bacon /** checksum for the `package.json` dependency object. */ 178d307f52SEvan Bacon dependencies: string; 184c50faceSEvan Bacon /** checksum for the `package.json` devDependency object. */ 198d307f52SEvan Bacon devDependencies: string; 208d307f52SEvan Bacon}; 218d307f52SEvan Bacon 224c50faceSEvan Baconconst PROJECT_PREBUILD_SETTINGS = '.expo/prebuild'; 234c50faceSEvan Baconconst CACHED_PACKAGE_JSON = 'cached-packages.json'; 244c50faceSEvan Bacon 254c50faceSEvan Baconfunction getTempPrebuildFolder(projectRoot: string): string { 264c50faceSEvan Bacon return path.join(projectRoot, PROJECT_PREBUILD_SETTINGS); 274c50faceSEvan Bacon} 284c50faceSEvan Bacon 294c50faceSEvan Baconfunction hasNewDependenciesSinceLastBuild( 304c50faceSEvan Bacon projectRoot: string, 314c50faceSEvan Bacon packageChecksums: PackageChecksums 324c50faceSEvan Bacon): boolean { 338d307f52SEvan Bacon // TODO: Maybe comparing lock files would be better... 348d307f52SEvan Bacon const templateDirectory = getTempPrebuildFolder(projectRoot); 358d307f52SEvan Bacon const tempPkgJsonPath = path.join(templateDirectory, CACHED_PACKAGE_JSON); 368d307f52SEvan Bacon if (!fs.existsSync(tempPkgJsonPath)) { 378d307f52SEvan Bacon return true; 388d307f52SEvan Bacon } 398d307f52SEvan Bacon const { dependencies, devDependencies } = JsonFile.read(tempPkgJsonPath); 408d307f52SEvan Bacon // Only change the dependencies if the normalized hash changes, this helps to reduce meaningless changes. 418d307f52SEvan Bacon const hasNewDependencies = packageChecksums.dependencies !== dependencies; 428d307f52SEvan Bacon const hasNewDevDependencies = packageChecksums.devDependencies !== devDependencies; 438d307f52SEvan Bacon 448d307f52SEvan Bacon return hasNewDependencies || hasNewDevDependencies; 458d307f52SEvan Bacon} 468d307f52SEvan Bacon 478d307f52SEvan Baconfunction createPackageChecksums(pkg: PackageJSONConfig): PackageChecksums { 488d307f52SEvan Bacon return { 498d307f52SEvan Bacon dependencies: hashForDependencyMap(pkg.dependencies || {}), 508d307f52SEvan Bacon devDependencies: hashForDependencyMap(pkg.devDependencies || {}), 518d307f52SEvan Bacon }; 528d307f52SEvan Bacon} 538d307f52SEvan Bacon 544c50faceSEvan Bacon/** @returns `true` if the package.json dependency hash does not match the cached hash from the last run. */ 554c50faceSEvan Baconexport async function hasPackageJsonDependencyListChangedAsync( 564c50faceSEvan Bacon projectRoot: string 574c50faceSEvan Bacon): Promise<boolean> { 588d307f52SEvan Bacon const pkg = getPackageJson(projectRoot); 598d307f52SEvan Bacon 608d307f52SEvan Bacon const packages = createPackageChecksums(pkg); 618d307f52SEvan Bacon const hasNewDependencies = hasNewDependenciesSinceLastBuild(projectRoot, packages); 628d307f52SEvan Bacon 638d307f52SEvan Bacon // Cache package.json 648d307f52SEvan Bacon await ensureDirectoryAsync(getTempPrebuildFolder(projectRoot)); 658d307f52SEvan Bacon const templateDirectory = path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON); 668d307f52SEvan Bacon await JsonFile.writeAsync(templateDirectory, packages); 678d307f52SEvan Bacon 688d307f52SEvan Bacon return hasNewDependencies; 698d307f52SEvan Bacon} 708d307f52SEvan Bacon 714c50faceSEvan Baconexport async function installCocoaPodsAsync(projectRoot: string): Promise<boolean> { 728d307f52SEvan Bacon let step = logNewSection('Installing CocoaPods...'); 738d307f52SEvan Bacon if (process.platform !== 'darwin') { 748d307f52SEvan Bacon step.succeed('Skipped installing CocoaPods because operating system is not on macOS.'); 758d307f52SEvan Bacon return false; 768d307f52SEvan Bacon } 778d307f52SEvan Bacon 788d307f52SEvan Bacon const packageManager = new PackageManager.CocoaPodsPackageManager({ 798d307f52SEvan Bacon cwd: path.join(projectRoot, 'ios'), 805417038cSCedric van Putten silent: !(env.EXPO_DEBUG || env.CI), 818d307f52SEvan Bacon }); 828d307f52SEvan Bacon 838d307f52SEvan Bacon if (!(await packageManager.isCLIInstalledAsync())) { 848d307f52SEvan Bacon try { 858d307f52SEvan Bacon // prompt user -- do you want to install cocoapods right now? 868d307f52SEvan Bacon step.text = 'CocoaPods CLI not found in your PATH, installing it now.'; 878d307f52SEvan Bacon step.stopAndPersist(); 888d307f52SEvan Bacon await PackageManager.CocoaPodsPackageManager.installCLIAsync({ 898d307f52SEvan Bacon nonInteractive: true, 908d307f52SEvan Bacon spawnOptions: { 918d307f52SEvan Bacon ...packageManager.options, 928d307f52SEvan Bacon // Don't silence this part 938d307f52SEvan Bacon stdio: ['inherit', 'inherit', 'pipe'], 948d307f52SEvan Bacon }, 958d307f52SEvan Bacon }); 968d307f52SEvan Bacon step.succeed('Installed CocoaPods CLI.'); 978d307f52SEvan Bacon step = logNewSection('Running `pod install` in the `ios` directory.'); 9829975bfdSEvan Bacon } catch (error: any) { 998d307f52SEvan Bacon step.stopAndPersist({ 1008d307f52SEvan Bacon symbol: '⚠️ ', 1018d307f52SEvan Bacon text: chalk.red('Unable to install the CocoaPods CLI.'), 1028d307f52SEvan Bacon }); 10329975bfdSEvan Bacon if (error instanceof PackageManager.CocoaPodsError) { 10429975bfdSEvan Bacon Log.log(error.message); 1058d307f52SEvan Bacon } else { 10629975bfdSEvan Bacon Log.log(`Unknown error: ${error.message}`); 1078d307f52SEvan Bacon } 1088d307f52SEvan Bacon return false; 1098d307f52SEvan Bacon } 1108d307f52SEvan Bacon } 1118d307f52SEvan Bacon 1128d307f52SEvan Bacon try { 113*1a3a1db5SEvan Bacon await packageManager.installAsync({ 114*1a3a1db5SEvan Bacon // @ts-expect-error: multiple versions in the monorepo 115*1a3a1db5SEvan Bacon spinner: step, 116*1a3a1db5SEvan Bacon }); 1178d307f52SEvan Bacon // Create cached list for later 1188d307f52SEvan Bacon await hasPackageJsonDependencyListChangedAsync(projectRoot).catch(() => null); 1198d307f52SEvan Bacon step.succeed('Installed pods and initialized Xcode workspace.'); 1208d307f52SEvan Bacon return true; 12129975bfdSEvan Bacon } catch (error: any) { 1228d307f52SEvan Bacon step.stopAndPersist({ 1238d307f52SEvan Bacon symbol: '⚠️ ', 1248d307f52SEvan Bacon text: chalk.red('Something went wrong running `pod install` in the `ios` directory.'), 1258d307f52SEvan Bacon }); 12629975bfdSEvan Bacon if (error instanceof PackageManager.CocoaPodsError) { 12729975bfdSEvan Bacon Log.log(error.message); 1288d307f52SEvan Bacon } else { 12929975bfdSEvan Bacon Log.log(`Unknown error: ${error.message}`); 1308d307f52SEvan Bacon } 1318d307f52SEvan Bacon return false; 1328d307f52SEvan Bacon } 1338d307f52SEvan Bacon} 134c4ef02aeSEvan Bacon 135c4ef02aeSEvan Baconfunction doesProjectUseCocoaPods(projectRoot: string): boolean { 136c4ef02aeSEvan Bacon return fs.existsSync(path.join(projectRoot, 'ios', 'Podfile')); 137c4ef02aeSEvan Bacon} 138c4ef02aeSEvan Bacon 139c4ef02aeSEvan Baconfunction isLockfileCreated(projectRoot: string): boolean { 140c4ef02aeSEvan Bacon const podfileLockPath = path.join(projectRoot, 'ios', 'Podfile.lock'); 141c4ef02aeSEvan Bacon return fs.existsSync(podfileLockPath); 142c4ef02aeSEvan Bacon} 143c4ef02aeSEvan Bacon 144c4ef02aeSEvan Baconfunction isPodFolderCreated(projectRoot: string): boolean { 145c4ef02aeSEvan Bacon const podFolderPath = path.join(projectRoot, 'ios', 'Pods'); 146c4ef02aeSEvan Bacon return fs.existsSync(podFolderPath); 147c4ef02aeSEvan Bacon} 148c4ef02aeSEvan Bacon 149c4ef02aeSEvan Bacon// TODO: Same process but with app.config changes + default plugins. 150c4ef02aeSEvan Bacon// This will ensure the user is prompted for extra setup. 151c4ef02aeSEvan Baconexport async function maybePromptToSyncPodsAsync(projectRoot: string) { 152c4ef02aeSEvan Bacon if (!doesProjectUseCocoaPods(projectRoot)) { 153c4ef02aeSEvan Bacon // Project does not use CocoaPods 154c4ef02aeSEvan Bacon return; 155c4ef02aeSEvan Bacon } 156c4ef02aeSEvan Bacon if (!isLockfileCreated(projectRoot) || !isPodFolderCreated(projectRoot)) { 157c4ef02aeSEvan Bacon if (!(await installCocoaPodsAsync(projectRoot))) { 158c4ef02aeSEvan Bacon throw new AbortCommandError(); 159c4ef02aeSEvan Bacon } 160c4ef02aeSEvan Bacon return; 161c4ef02aeSEvan Bacon } 162c4ef02aeSEvan Bacon 163c4ef02aeSEvan Bacon // Getting autolinked packages can be heavy, optimize around checking every time. 164c4ef02aeSEvan Bacon if (!(await hasPackageJsonDependencyListChangedAsync(projectRoot))) { 165c4ef02aeSEvan Bacon return; 166c4ef02aeSEvan Bacon } 167c4ef02aeSEvan Bacon 168c4ef02aeSEvan Bacon await promptToInstallPodsAsync(projectRoot, []); 169c4ef02aeSEvan Bacon} 170c4ef02aeSEvan Bacon 171c4ef02aeSEvan Baconasync function promptToInstallPodsAsync(projectRoot: string, missingPods?: string[]) { 172c4ef02aeSEvan Bacon if (missingPods?.length) { 173c4ef02aeSEvan Bacon Log.log( 174c4ef02aeSEvan Bacon `Could not find the following native modules: ${missingPods 175c4ef02aeSEvan Bacon .map((pod) => chalk.bold(pod)) 176c4ef02aeSEvan Bacon .join(', ')}. Did you forget to run "${chalk.bold('pod install')}" ?` 177c4ef02aeSEvan Bacon ); 178c4ef02aeSEvan Bacon } 179c4ef02aeSEvan Bacon 180c4ef02aeSEvan Bacon try { 181c4ef02aeSEvan Bacon if (!(await installCocoaPodsAsync(projectRoot))) { 182c4ef02aeSEvan Bacon throw new AbortCommandError(); 183c4ef02aeSEvan Bacon } 184c4ef02aeSEvan Bacon } catch (error) { 185c4ef02aeSEvan Bacon await fs.promises.rm(path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON), { 186c4ef02aeSEvan Bacon recursive: true, 187c4ef02aeSEvan Bacon force: true, 188c4ef02aeSEvan Bacon }); 189c4ef02aeSEvan Bacon throw error; 190c4ef02aeSEvan Bacon } 191c4ef02aeSEvan Bacon} 192