xref: /expo/packages/@expo/cli/src/utils/cocoapods.ts (revision 1a3a1db5)
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