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