xref: /expo/packages/@expo/cli/src/utils/cocoapods.ts (revision b48ca2c7)
1import { getPackageJson, PackageJSONConfig } from '@expo/config';
2import JsonFile from '@expo/json-file';
3import * as PackageManager from '@expo/package-manager';
4import chalk from 'chalk';
5import fs from 'fs';
6import path from 'path';
7
8import * as Log from '../log';
9import { hashForDependencyMap } from '../prebuild/updatePackageJson';
10import { ensureDirectoryAsync } from './dir';
11import { env } from './env';
12import { logNewSection } from './ora';
13
14type PackageChecksums = {
15  /** checksum for the `package.json` dependency object. */
16  dependencies: string;
17  /** checksum for the `package.json` devDependency object. */
18  devDependencies: string;
19};
20
21const PROJECT_PREBUILD_SETTINGS = '.expo/prebuild';
22const CACHED_PACKAGE_JSON = 'cached-packages.json';
23
24function getTempPrebuildFolder(projectRoot: string): string {
25  return path.join(projectRoot, PROJECT_PREBUILD_SETTINGS);
26}
27
28function hasNewDependenciesSinceLastBuild(
29  projectRoot: string,
30  packageChecksums: PackageChecksums
31): boolean {
32  // TODO: Maybe comparing lock files would be better...
33  const templateDirectory = getTempPrebuildFolder(projectRoot);
34  const tempPkgJsonPath = path.join(templateDirectory, CACHED_PACKAGE_JSON);
35  if (!fs.existsSync(tempPkgJsonPath)) {
36    return true;
37  }
38  const { dependencies, devDependencies } = JsonFile.read(tempPkgJsonPath);
39  // Only change the dependencies if the normalized hash changes, this helps to reduce meaningless changes.
40  const hasNewDependencies = packageChecksums.dependencies !== dependencies;
41  const hasNewDevDependencies = packageChecksums.devDependencies !== devDependencies;
42
43  return hasNewDependencies || hasNewDevDependencies;
44}
45
46function createPackageChecksums(pkg: PackageJSONConfig): PackageChecksums {
47  return {
48    dependencies: hashForDependencyMap(pkg.dependencies || {}),
49    devDependencies: hashForDependencyMap(pkg.devDependencies || {}),
50  };
51}
52
53/** @returns `true` if the package.json dependency hash does not match the cached hash from the last run. */
54export async function hasPackageJsonDependencyListChangedAsync(
55  projectRoot: string
56): Promise<boolean> {
57  const pkg = getPackageJson(projectRoot);
58
59  const packages = createPackageChecksums(pkg);
60  const hasNewDependencies = hasNewDependenciesSinceLastBuild(projectRoot, packages);
61
62  // Cache package.json
63  await ensureDirectoryAsync(getTempPrebuildFolder(projectRoot));
64  const templateDirectory = path.join(getTempPrebuildFolder(projectRoot), CACHED_PACKAGE_JSON);
65  await JsonFile.writeAsync(templateDirectory, packages);
66
67  return hasNewDependencies;
68}
69
70export async function installCocoaPodsAsync(projectRoot: string): Promise<boolean> {
71  let step = logNewSection('Installing CocoaPods...');
72  if (process.platform !== 'darwin') {
73    step.succeed('Skipped installing CocoaPods because operating system is not on macOS.');
74    return false;
75  }
76
77  const packageManager = new PackageManager.CocoaPodsPackageManager({
78    cwd: path.join(projectRoot, 'ios'),
79    silent: !env.EXPO_DEBUG,
80  });
81
82  if (!(await packageManager.isCLIInstalledAsync())) {
83    try {
84      // prompt user -- do you want to install cocoapods right now?
85      step.text = 'CocoaPods CLI not found in your PATH, installing it now.';
86      step.stopAndPersist();
87      await PackageManager.CocoaPodsPackageManager.installCLIAsync({
88        nonInteractive: true,
89        spawnOptions: {
90          ...packageManager.options,
91          // Don't silence this part
92          stdio: ['inherit', 'inherit', 'pipe'],
93        },
94      });
95      step.succeed('Installed CocoaPods CLI.');
96      step = logNewSection('Running `pod install` in the `ios` directory.');
97    } catch (error: any) {
98      step.stopAndPersist({
99        symbol: '⚠️ ',
100        text: chalk.red('Unable to install the CocoaPods CLI.'),
101      });
102      if (error instanceof PackageManager.CocoaPodsError) {
103        Log.log(error.message);
104      } else {
105        Log.log(`Unknown error: ${error.message}`);
106      }
107      return false;
108    }
109  }
110
111  try {
112    await packageManager.installAsync({ spinner: step });
113    // Create cached list for later
114    await hasPackageJsonDependencyListChangedAsync(projectRoot).catch(() => null);
115    step.succeed('Installed pods and initialized Xcode workspace.');
116    return true;
117  } catch (error: any) {
118    step.stopAndPersist({
119      symbol: '⚠️ ',
120      text: chalk.red('Something went wrong running `pod install` in the `ios` directory.'),
121    });
122    if (error instanceof PackageManager.CocoaPodsError) {
123      Log.log(error.message);
124    } else {
125      Log.log(`Unknown error: ${error.message}`);
126    }
127    return false;
128  }
129}
130