xref: /expo/tools/src/versioning/ios/index.ts (revision 895fafd4)
1import spawnAsync from '@expo/spawn-async';
2import assert from 'assert';
3import chalk from 'chalk';
4import { PromisyClass, TaskQueue } from 'cwait';
5import fs from 'fs-extra';
6import glob from 'glob-promise';
7import inquirer from 'inquirer';
8import path from 'path';
9import semver from 'semver';
10
11import { runReactNativeCodegenAsync } from '../../Codegen';
12import {
13  EXPO_DIR,
14  IOS_DIR,
15  REACT_NATIVE_SUBMODULE_DIR,
16  REACT_NATIVE_SUBMODULE_MONOREPO_ROOT,
17  VERSIONED_RN_IOS_DIR,
18} from '../../Constants';
19import logger from '../../Logger';
20import { getListOfPackagesAsync, Package } from '../../Packages';
21import { copyFileWithTransformsAsync } from '../../Transforms';
22import type { FileTransforms, StringTransform } from '../../Transforms.types';
23import { renderExpoKitPodspecAsync } from '../../dynamic-macros/IosMacrosGenerator';
24import { runTransformPipelineAsync } from './transforms';
25import { injectMacros } from './transforms/injectMacros';
26import { kernelFilesTransforms } from './transforms/kernelFilesTransforms';
27import { podspecTransforms } from './transforms/podspecTransforms';
28import { postTransforms } from './transforms/postTransforms';
29import { getVersionedDirectory, getVersionedExpoKitPath } from './utils';
30import { versionExpoModulesAsync } from './versionExpoModules';
31import {
32  MODULES_PROVIDER_POD_NAME,
33  versionExpoModulesProviderAsync,
34} from './versionExpoModulesProvider';
35import { createVersionedHermesTarball } from './versionHermes';
36import {
37  versionVendoredModulesAsync,
38  removeVersionedVendoredModulesAsync,
39} from './versionVendoredModules';
40
41export { versionVendoredModulesAsync, versionExpoModulesAsync };
42
43const UNVERSIONED_PLACEHOLDER = '__UNVERSIONED__';
44const RELATIVE_RN_PATH = path.relative(EXPO_DIR, REACT_NATIVE_SUBMODULE_DIR);
45
46const EXTERNAL_REACT_ABI_DEPENDENCIES = [
47  'Analytics',
48  'AppAuth',
49  'FBAudienceNetwork',
50  'FBSDKCoreKit',
51  'GoogleSignIn',
52  'GoogleMaps',
53  'Google-Maps-iOS-Utils',
54  'lottie-ios',
55  'JKBigInteger',
56  'Branch',
57  'Google-Mobile-Ads-SDK',
58  'RCT-Folly',
59];
60
61const EXCLUDED_POD_DEPENDENCIES = ['ExpoModulesTestCore'];
62
63/**
64 *  Transform and rename the given react native source code files.
65 *  @param filenames list of files to transform
66 *  @param versionPrefix A version-specific prefix to apply to all symbols in the code, e.g.
67 *    RCTSomeClass becomes {versionPrefix}RCTSomeClass
68 *  @param versionedPodNames mapping from unversioned cocoapods names to versioned cocoapods names,
69 *    e.g. React -> ReactABI99_0_0
70 */
71async function namespaceReactNativeFilesAsync(filenames, versionPrefix, versionedPodNames) {
72  const reactPodName = versionedPodNames.React;
73  const transformRules = _getReactNativeTransformRules(versionPrefix, reactPodName);
74  const taskQueue = new TaskQueue(Promise as PromisyClass, 4); // Transform up to 4 files simultaneously.
75  const transformRulesCache = {};
76
77  const transformSingleFile = taskQueue.wrap(async (filename) => {
78    if (_isDirectory(filename)) {
79      return;
80    }
81    // protect contents of EX_UNVERSIONED macro
82    const unversionedCaptures: string[] = [];
83    await _transformFileContentsAsync(filename, (fileString) => {
84      const pattern = /EX_UNVERSIONED\((.*?)\)/g;
85      let match = pattern.exec(fileString);
86      while (match != null) {
87        unversionedCaptures.push(match[1]);
88        match = pattern.exec(fileString);
89      }
90      if (unversionedCaptures.length) {
91        return fileString.replace(pattern, UNVERSIONED_PLACEHOLDER);
92      }
93      return null;
94    });
95
96    // rename file
97    const dirname = path.dirname(filename);
98    const basename = path.basename(filename);
99    const versionedBasename = !basename.startsWith(versionPrefix)
100      ? `${versionPrefix}${basename}`
101      : basename;
102    const targetPath = path.join(dirname, versionedBasename);
103
104    // filter transformRules to patterns which apply to this dirname
105    const filteredTransformRules =
106      transformRulesCache[dirname] || _getTransformRulesForDirname(transformRules, dirname);
107    transformRulesCache[dirname] = filteredTransformRules;
108
109    // Perform sed find & replace.
110    for (const rule of filteredTransformRules) {
111      await spawnAsync('sed', [rule.flags || '-i', '--', rule.pattern, filename]);
112    }
113
114    // Rename file to be prefixed.
115    if (filename !== targetPath) {
116      await fs.move(filename, targetPath);
117    }
118
119    // perform transforms that sed can't express
120    await _transformFileContentsAsync(targetPath, async (fileString) => {
121      // rename misc imports, e.g. Layout.h
122      fileString = fileString.replace(
123        /#(include|import)\s+"((?:[^"\/]+\/)?)([^"]+\.h)"/g,
124        (match, p1, p2, p3) => {
125          return p3.startsWith(versionPrefix) ? match : `#${p1} "${p2}${versionPrefix}${p3}"`;
126        }
127      );
128
129      // [hermes] the transform above will replace
130      // #include "hermes/inspector/detail/Thread.h" -> #include "hermes/ABIX_0_0inspector/detail/Thread.h"
131      // that is not correct.
132      // because hermes podspec doesn't use header_dir, we only use the header basename for versioning.
133      // this transform would replace
134      // #include "hermes/ABIX_0_0inspector/detail/Thread.h" -> #include "hermes/inspector/detail/ABIX_0_0Thread.h"
135      // note that the rule should be placed after the "rename misc imports" transform.
136      fileString = fileString.replace(
137        new RegExp(`^(#import|#include\\s+["<])(${versionPrefix}hermes\\/.+\\.h)([">])$`, 'gm'),
138        (match, prefix, header, suffix) => {
139          const headers = header.split('/').map((part) => part.replace(versionPrefix, ''));
140          assert(headers.length > 1);
141          const lastPart = headers[headers.length - 1];
142          headers[headers.length - 1] = `${versionPrefix}${lastPart}`;
143          return `${prefix}${headers.join('/')}${suffix}`;
144        }
145      );
146
147      // restore EX_UNVERSIONED contents
148      if (unversionedCaptures) {
149        let index = 0;
150        do {
151          fileString = fileString.replace(UNVERSIONED_PLACEHOLDER, unversionedCaptures[index]);
152          index++;
153        } while (fileString.indexOf(UNVERSIONED_PLACEHOLDER) !== -1);
154      }
155
156      const injectedMacrosOutput = await runTransformPipelineAsync({
157        pipeline: injectMacros(versionPrefix),
158        input: fileString,
159        targetPath,
160      });
161
162      return await runTransformPipelineAsync({
163        pipeline: postTransforms(versionPrefix),
164        input: injectedMacrosOutput,
165        targetPath,
166      });
167    });
168    // process `filename`
169  });
170
171  await Promise.all(filenames.map(transformSingleFile));
172}
173
174/**
175 *  Transform and rename all code files we care about under `rnPath`
176 */
177async function transformReactNativeAsync(rnPath, versionName, versionedPodNames) {
178  const filenameQueries = [`${rnPath}/**/*.[hmSc]`, `${rnPath}/**/*.mm`, `${rnPath}/**/*.cpp`];
179  let filenames: string[] = [];
180  await Promise.all(
181    filenameQueries.map(async (query) => {
182      const queryFilenames = (await glob(query)) as string[];
183      if (queryFilenames) {
184        filenames = filenames.concat(queryFilenames);
185      }
186    })
187  );
188
189  return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames);
190}
191
192/**
193 * For all files matching the given glob query, namespace and rename them
194 * with the given version number. This utility is mainly useful for backporting
195 * small changes into an existing SDK. To create a new SDK version, use `addVersionAsync`
196 * instead.
197 * @param globQuery a string to pass to glob which matches some file paths
198 * @param versionNumber Exponent SDK version, e.g. 42.0.0
199 */
200export async function versionReactNativeIOSFilesAsync(globQuery, versionNumber) {
201  const filenames = await glob(globQuery);
202  if (!filenames || !filenames.length) {
203    throw new Error(`No files matched the given pattern: ${globQuery}`);
204  }
205  const { versionName, versionedPodNames } = await getConfigsFromArguments(versionNumber);
206  console.log(`Versioning ${filenames.length} files with SDK version ${versionNumber}...`);
207  return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames);
208}
209
210async function generateVersionedReactNativeAsync(versionName: string): Promise<void> {
211  const versionedReactNativePath = getVersionedReactNativePath(versionName);
212
213  await fs.mkdirs(versionedReactNativePath);
214
215  // Clone react native latest version
216  console.log(`Copying files from ${chalk.magenta(RELATIVE_RN_PATH)} ...`);
217
218  const filesToCopy = [
219    'React',
220    'Libraries',
221    'React.podspec',
222    'React-Core.podspec',
223    'ReactCommon/ReactCommon.podspec',
224    'ReactCommon/React-Fabric.podspec',
225    'ReactCommon/React-rncore.podspec',
226    'ReactCommon/hermes/React-hermes.podspec',
227    'sdks/hermes-engine/hermes-engine.podspec',
228    'package.json',
229  ];
230
231  for (const fileToCopy of filesToCopy) {
232    await fs.copy(
233      path.join(EXPO_DIR, RELATIVE_RN_PATH, fileToCopy),
234      path.join(versionedReactNativePath, fileToCopy)
235    );
236  }
237
238  console.log(`Removing unnecessary ${chalk.magenta('*.js')} files ...`);
239
240  const jsFiles = (await glob(path.join(versionedReactNativePath, '**', '*.js'))) as string[];
241
242  for (const jsFile of jsFiles) {
243    await fs.remove(jsFile);
244  }
245  await Promise.all(jsFiles.map((jsFile) => fs.remove(jsFile)));
246
247  console.log('Running react-native-codegen');
248  await runReactNativeCodegenAsync({
249    reactNativeRoot: path.join(EXPO_DIR, RELATIVE_RN_PATH),
250    codegenPkgRoot: path.join(
251      REACT_NATIVE_SUBMODULE_MONOREPO_ROOT,
252      'packages',
253      'react-native-codegen'
254    ),
255    outputDir: path.join(versionedReactNativePath, 'codegen', 'ios'),
256    name: `${versionName}FBReactNativeSpec`,
257    type: 'modules',
258    platform: 'ios',
259    jsSrcsDir: path.join(EXPO_DIR, RELATIVE_RN_PATH, 'Libraries'),
260    keepIntermediateSchema: true,
261  });
262  console.log(`Removing unused generated FBReactNativeSpecJSI files for 0.72`);
263  await Promise.all(
264    [
265      `${versionName}FBReactNativeSpecJSI.h`,
266      `${versionName}FBReactNativeSpecJSI-generated.cpp`,
267    ].map((file) => {
268      const filePath = path.join(versionedReactNativePath, 'codegen', 'ios', file);
269      return fs.remove(filePath);
270    })
271  );
272
273  console.log(
274    `Copying cpp libraries from ${chalk.magenta(path.join(RELATIVE_RN_PATH, 'ReactCommon'))} ...`
275  );
276  const cppLibraries = getCppLibrariesToVersion();
277
278  await fs.mkdirs(path.join(versionedReactNativePath, 'ReactCommon'));
279
280  for (const library of cppLibraries) {
281    await fs.copy(
282      path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', library.libName),
283      path.join(versionedReactNativePath, 'ReactCommon', library.libName)
284    );
285  }
286  // remove hermes test files in ReactCommon/hermes copied above
287  const hermesTestFiles = await glob('**/{cli,tests,tools}', {
288    cwd: path.join(versionedReactNativePath, 'ReactCommon', 'hermes'),
289    absolute: true,
290  });
291  await Promise.all(hermesTestFiles.map((file) => fs.remove(file)));
292
293  await generateReactNativePodScriptAsync(versionedReactNativePath, versionName);
294  await generateReactNativePodspecsAsync(versionedReactNativePath, versionName);
295}
296
297/**
298 * There are some kernel files that unfortunately have to call versioned code directly.
299 * This function applies the specified changes in the kernel codebase.
300 * The nature of kernel modifications is that they are temporary and at one point these have to be rollbacked.
301 * @param versionName SDK version, e.g. 21.0.0, 37.0.0, etc.
302 * @param rollback flag indicating whether to invoke rollbacking modification.
303 */
304async function modifyKernelFilesAsync(
305  versionName: string,
306  rollback: boolean = false
307): Promise<void> {
308  const kernelFilesPath = path.join(IOS_DIR, 'Exponent/kernel');
309  const filenameQueries = [`${kernelFilesPath}/**/EXAppViewController.m`];
310  let filenames: string[] = [];
311  await Promise.all(
312    filenameQueries.map(async (query) => {
313      const queryFilenames = (await glob(query)) as string[];
314      if (queryFilenames) {
315        filenames = filenames.concat(queryFilenames);
316      }
317    })
318  );
319  await Promise.all(
320    filenames.map(async (filename) => {
321      console.log(`Modifying ${chalk.magenta(path.relative(EXPO_DIR, filename))}:`);
322      await _transformFileContentsAsync(filename, (fileContents) =>
323        runTransformPipelineAsync({
324          pipeline: kernelFilesTransforms(versionName, rollback),
325          targetPath: filename,
326          input: fileContents,
327        })
328      );
329    })
330  );
331}
332/**
333 * - Copies `scripts/react_native_pods.rb` script into versioned ReactNative directory.
334 * - Removes pods installed from third-party-podspecs (we don't version them).
335 * - Versions `use_react_native` method and all pods it declares.
336 */
337async function generateReactNativePodScriptAsync(
338  versionedReactNativePath: string,
339  versionName: string
340): Promise<void> {
341  const reactCodegenDependencies = [
342    'FBReactNativeSpec',
343    'React-jsiexecutor',
344    'RCTRequired',
345    'RCTTypeSafety',
346    'React-Core',
347    'React-jsi',
348    'React-NativeModulesApple',
349    'ReactCommon/turbomodule/core',
350    'ReactCommon/turbomodule/bridging',
351    'React-graphics',
352    'React-rncore',
353    'hermes-engine',
354    'React-jsc',
355  ];
356
357  const reactNativePodScriptTransforms: StringTransform[] = [
358    {
359      find: /\b(def (use_react_native|use_react_native_codegen|setup_jsc))!/g,
360      replaceWith: `$1_${versionName}!`,
361    },
362    {
363      find: /(\bpod\s+([^\n]+)\/third-party-podspecs\/([^\n]+))/g,
364      replaceWith: '# $1',
365    },
366    {
367      find: /\bpod\s+'([^\']+)'/g,
368      replaceWith: `pod '${versionName}$1'`,
369    },
370    {
371      find: /(:path => "[^"]+")/g,
372      replaceWith: `$1, :project_name => '${versionName}'`,
373    },
374
375    // Removes duplicated constants
376    {
377      find: "DEFAULT_OTHER_CPLUSPLUSFLAGS = '$(inherited)'",
378      replaceWith: '',
379    },
380    {
381      find: "NEW_ARCH_OTHER_CPLUSPLUSFLAGS = '$(inherited) -DRCT_NEW_ARCH_ENABLED=1 -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'",
382      replaceWith: '',
383    },
384
385    // Since `React-Codegen.podspec` is generated during `pod install`, versioning should be done in the pod script.
386    {
387      find: "$CODEGEN_OUTPUT_DIR = 'build/generated/ios'",
388      replaceWith: `$CODEGEN_OUTPUT_DIR = '${path.relative(
389        IOS_DIR,
390        versionedReactNativePath
391      )}/codegen/ios'`,
392    },
393    {
394      find: /\$(CODEGEN_OUTPUT_DIR)\b/g,
395      replaceWith: `$${versionName}$1`,
396    },
397    { find: /\b(React-Codegen)\b/g, replaceWith: `${versionName}$1` },
398    { find: /(\$\(PODS_ROOT\)\/Headers\/Private\/)React-/g, replaceWith: `$1${versionName}React-` },
399    {
400      find: /^\s+CodegenUtils\.clean_up_build_folder\(.+$/gm,
401      replaceWith: '',
402    },
403    {
404      find: /^\s+build_codegen!\(.+$/gm,
405      replaceWith: '',
406    },
407  ];
408
409  const hermesVersion = await fs.readFile(
410    path.join(REACT_NATIVE_SUBMODULE_DIR, 'sdks', '.hermesversion'),
411    'utf8'
412  );
413  const hermesTransforms: StringTransform[] = [
414    { find: /^\s+prepare_hermes[.\s\S]*abort unless prep_status == 0\n$/gm, replaceWith: '' },
415    {
416      find: new RegExp(
417        `^\\s*pod '${versionName}hermes-engine', :podspec => "#\\{react_native_path\\}\\/sdks\\/hermes-engine\\/hermes-engine.podspec", :tag => hermestag`,
418        'gm'
419      ),
420      replaceWith: `
421    if File.exist?("#{react_native_path}/sdks/hermes-engine/destroot")
422      pod '${versionName}hermes-engine', :path => "#{react_native_path}/sdks/hermes-engine", :project_name => '${versionName}', :tag => '${hermesVersion}'
423    else
424      pod '${versionName}hermes-engine', :podspec => "#{react_native_path}/sdks/hermes-engine/${versionName}hermes-engine.podspec", :project_name => '${versionName}', :tag => '${hermesVersion}'
425    end`,
426    },
427    { find: new RegExp(`\\b${versionName}(libevent)\\b`, 'g'), replaceWith: '$1' },
428  ];
429
430  const commonMethodTransforms = [
431    'get_script_phases_with_codegen_discovery',
432    'get_script_phases_no_codegen_discovery',
433    'get_script_template',
434    'setup_jsc',
435    'setup_hermes',
436    'run_codegen',
437  ];
438
439  const transforms: FileTransforms = {
440    content: [
441      ...reactNativePodScriptTransforms.map((stringTransform) => ({
442        path: 'react_native_pods.rb',
443        ...stringTransform,
444      })),
445      ...hermesTransforms.map((stringTransform) => ({
446        paths: 'jsengine.rb',
447        ...stringTransform,
448      })),
449      {
450        paths: 'codegen_utils.rb',
451        find: new RegExp(`["'](${reactCodegenDependencies.join('|')})["']:(\\s*\\[\\],?)`, 'g'),
452        replaceWith: `"${versionName}$1":$2`,
453      },
454      {
455        paths: [
456          'react_native_pods.rb',
457          'script_phases.rb',
458          'jsengine.rb',
459          'codegen.rb',
460          'codegen_utils.rb',
461        ],
462        find: new RegExp(`\\b(${commonMethodTransforms.join('|')})\\b`, 'g'),
463        replaceWith: `$1_${versionName}`,
464      },
465    ],
466  };
467
468  const reactNativeScriptsDir = path.join(EXPO_DIR, RELATIVE_RN_PATH, 'scripts');
469  const scriptFiles = await glob('**/*', { cwd: reactNativeScriptsDir, nodir: true, dot: true });
470  await Promise.all(
471    scriptFiles.map(async (file) => {
472      await copyFileWithTransformsAsync({
473        sourceFile: file,
474        sourceDirectory: reactNativeScriptsDir,
475        targetDirectory: path.join(versionedReactNativePath, 'scripts'),
476        transforms,
477        keepFileMode: true,
478      });
479    })
480  );
481
482  await fs.copy(
483    path.join(EXPO_DIR, RELATIVE_RN_PATH, 'sdks', 'hermes-engine', 'hermes-utils.rb'),
484    path.join(versionedReactNativePath, 'sdks', 'hermes-engine', 'hermes-utils.rb')
485  );
486}
487
488async function generateReactNativePodspecsAsync(
489  versionedReactNativePath: string,
490  versionName: string
491): Promise<void> {
492  const podspecFiles = await glob(path.join(versionedReactNativePath, '**', '*.podspec'));
493
494  for (const podspecFile of podspecFiles) {
495    const basename = path.basename(podspecFile, '.podspec');
496
497    if (/^react$/i.test(basename)) {
498      continue;
499    }
500
501    console.log(
502      `Generating podspec for ${chalk.green(basename)} at ${chalk.magenta(
503        path.relative(versionedReactNativePath, podspecFile)
504      )} ...`
505    );
506
507    const podspecSource = await fs.readFile(podspecFile, 'utf8');
508
509    const podspecOutput = await runTransformPipelineAsync({
510      pipeline: podspecTransforms(versionName),
511      input: podspecSource,
512      targetPath: podspecFile,
513    });
514
515    // Write transformed podspec output to the prefixed file.
516    await fs.writeFile(
517      path.join(path.dirname(podspecFile), `${versionName}${basename}.podspec`),
518      podspecOutput
519    );
520
521    // Remove original and unprefixed podspec.
522    await fs.remove(podspecFile);
523  }
524
525  await generateReactPodspecAsync(versionedReactNativePath, versionName);
526}
527
528/**
529 * @param versionName Version prefix (e.g. `ABI43_0_0`)
530 * @param sdkNumber Major version of the SDK
531 */
532async function generateVersionedExpoAsync(versionName: string, sdkNumber: number): Promise<void> {
533  const versionedExpoKitPath = getVersionedExpoKitPath(versionName);
534  const versionedUnimodulePods = await getVersionedUnimodulePodsAsync(versionName);
535
536  await fs.mkdirs(versionedExpoKitPath);
537
538  // Copy versioned exponent modules into the clone
539  console.log(`Copying versioned native modules into the new Pod...`);
540
541  await fs.copy(path.join(IOS_DIR, 'Exponent', 'Versioned'), versionedExpoKitPath);
542
543  await fs.copy(
544    path.join(EXPO_DIR, 'ios', 'ExpoKit.podspec'),
545    path.join(versionedExpoKitPath, 'ExpoKit.podspec')
546  );
547
548  console.log(`Generating podspec for ${chalk.green('ExpoKit')} ...`);
549
550  await generateExpoKitPodspecAsync(
551    versionedExpoKitPath,
552    versionedUnimodulePods,
553    versionName,
554    `${sdkNumber}.0.0`
555  );
556
557  logger.info('�� Generating Swift modules provider');
558
559  await versionExpoModulesProviderAsync(sdkNumber);
560}
561
562/**
563 * Transforms ExpoKit.podspec, versioning Expo namespace, React pod name, replacing original ExpoKit podspecs
564 * with Expo and ExpoOptional.
565 * @param specfilePath location of ExpoKit.podspec to modify, e.g. /versioned-react-native/someversion/
566 * @param versionedReactPodName name of the new pod (and podfile)
567 * @param universalModulesPodNames versioned names of universal modules
568 * @param versionNumber "XX.X.X"
569 */
570async function generateExpoKitPodspecAsync(
571  specfilePath: string,
572  universalModulesPodNames: { [key: string]: string },
573  versionName: string,
574  versionNumber: string
575): Promise<void> {
576  const versionedReactPodName = getVersionedReactPodName(versionName);
577  const versionedExpoKitPodName = getVersionedExpoKitPodName(versionName);
578  const specFilename = path.join(specfilePath, 'ExpoKit.podspec');
579
580  // rename spec to newPodName
581  const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"ExpoKit"/\\1"${versionedExpoKitPodName}"/g`;
582
583  await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]);
584
585  // further processing that sed can't do very well
586  await _transformFileContentsAsync(specFilename, async (fileString) => {
587    // `universalModulesPodNames` contains only versioned unimodules,
588    // so we fall back to the original name if the module is not there
589    const universalModulesDependencies = (await getListOfPackagesAsync())
590      .filter(
591        (pkg) =>
592          pkg.isIncludedInExpoClientOnPlatform('ios') &&
593          pkg.podspecName &&
594          !EXCLUDED_POD_DEPENDENCIES.includes(pkg.podspecName)
595      )
596      .map(
597        ({ podspecName }) =>
598          `ss.dependency         "${universalModulesPodNames[podspecName!] || podspecName}"`
599      ).join(`
600    `);
601    const externalDependencies = EXTERNAL_REACT_ABI_DEPENDENCIES.map(
602      (podName) => `ss.dependency         "${podName}"`
603    ).join(`
604    `);
605    const subspec = `s.subspec "Expo" do |ss|
606    ss.source_files     = "Core/**/*.{h,m,mm,cpp}"
607
608    ss.dependency         "${versionedReactPodName}-Core"
609    ss.dependency         "${versionedReactPodName}-Core/DevSupport"
610    ss.dependency         "${versionedReactPodName}Common"
611    ss.dependency         "${versionName}RCTRequired"
612    ss.dependency         "${versionName}RCTTypeSafety"
613    ss.dependency         "${versionName}React-hermes"
614    ${universalModulesDependencies}
615    ${externalDependencies}
616    ss.dependency         "${versionName}${MODULES_PROVIDER_POD_NAME}"
617  end
618
619  s.subspec "ExpoOptional" do |ss|
620    ss.dependency         "${versionedExpoKitPodName}/Expo"
621    ss.source_files     = "Optional/**/*.{h,m,mm}"
622  end`;
623    fileString = fileString.replace(
624      /(s\.subspec ".+?"[\S\s]+?(?=end\b)end\b[\s]+)+/g,
625      `${subspec}\n`
626    );
627
628    // correct version number
629    fileString = fileString.replace(/(?<=s.version = ").*?(?=")/g, versionNumber);
630
631    // add Reanimated V2 RCT-Folly dependency
632    fileString = fileString.replace(
633      /(?=  s.subspec "Expo" do \|ss\|)/g,
634      `
635  header_search_paths = [
636    '"$(PODS_ROOT)/boost"',
637    '"$(PODS_ROOT)/glog"',
638    '"$(PODS_ROOT)/DoubleConversion"',
639    '"$(PODS_ROOT)/RCT-Folly"',
640    '"$(PODS_ROOT)/Headers/Private/${versionName}React-Core"',
641    '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}ExpoModulesCore/Swift Compatibility Header"',
642    '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXManifests/Swift Compatibility Header"',
643    '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXUpdatesInterface/Swift Compatibility Header"',
644    '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXUpdates/Swift Compatibility Header"',
645  ]
646  s.pod_target_xcconfig    = {
647    "CLANG_CXX_LANGUAGE_STANDARD" => "c++17",
648    "USE_HEADERMAP"       => "YES",
649    "DEFINES_MODULE"      => "YES",
650    "HEADER_SEARCH_PATHS" => header_search_paths.join(' '),
651  }
652  \n\n`
653    );
654
655    return fileString;
656  });
657
658  // move podspec to ${versionedExpoKitPodName}.podspec
659  await fs.move(specFilename, path.join(specfilePath, `${versionedExpoKitPodName}.podspec`));
660}
661
662/**
663 *  @param specfilePath location of React.podspec to modify, e.g. /versioned-react-native/someversion/
664 *  @param versionedReactPodName name of the new pod (and podfile)
665 */
666async function generateReactPodspecAsync(versionedReactNativePath, versionName) {
667  const versionedReactPodName = getVersionedReactPodName(versionName);
668  const versionedYogaPodName = getVersionedYogaPodName(versionName);
669  const versionedJSIPodName = getVersionedJSIPodName(versionName);
670  const specFilename = path.join(versionedReactNativePath, 'React.podspec');
671
672  // rename spec to newPodName
673  const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"React"/\\1"${versionedReactPodName}"/g`;
674  await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]);
675
676  // rename header_dir
677  await spawnAsync('sed', [
678    '-i',
679    '--',
680    `s/^\\(.*header_dir.*\\)React\\(.*\\)$/\\1${versionedReactPodName}\\2/`,
681    specFilename,
682  ]);
683  await spawnAsync('sed', [
684    '-i',
685    '--',
686    `s/^\\(.*header_dir.*\\)jsireact\\(.*\\)$/\\1${versionedJSIPodName}\\2/`,
687    specFilename,
688  ]);
689
690  // point source at .
691  const newPodSource = `{ :path => "." }`;
692  await spawnAsync('sed', [
693    '-i',
694    '--',
695    `s/\\(s\\.source[[:space:]]*=[[:space:]]\\).*/\\1${newPodSource}/g`,
696    specFilename,
697  ]);
698
699  // further processing that sed can't do very well
700  await _transformFileContentsAsync(specFilename, (fileString) => {
701    // replace React/* dependency with ${versionedReactPodName}/*
702    fileString = fileString.replace(
703      /(\.dependency\s+)"React([^"]+)"/g,
704      `$1"${versionedReactPodName}$2"`
705    );
706
707    fileString = fileString.replace('/RCTTV', `/${versionName}RCTTV`);
708
709    // namespace cpp libraries
710    const cppLibraries = getCppLibrariesToVersion();
711    cppLibraries.forEach(({ libName }) => {
712      fileString = fileString.replace(
713        new RegExp(`([^A-Za-z0-9_])${libName}([^A-Za-z0-9_])`, 'g'),
714        `$1${getVersionedLibraryName(libName, versionName)}$2`
715      );
716    });
717
718    // fix wrong Yoga pod name
719    fileString = fileString.replace(
720      /^(.*dependency.*["']).*yoga.*?(["'].*)$/m,
721      `$1${versionedYogaPodName}$2`
722    );
723
724    return fileString;
725  });
726
727  // move podspec to ${versionedReactPodName}.podspec
728  await fs.move(
729    specFilename,
730    path.join(versionedReactNativePath, `${versionedReactPodName}.podspec`)
731  );
732}
733
734function getCFlagsToPrefixGlobals(prefix, globals) {
735  return globals.map((val) => `-D${val}=${prefix}${val}`);
736}
737
738/**
739 * Generates `dependencies.rb` and `postinstalls.rb` files for versioned code.
740 * @param versionNumber Semver-compliant version of the SDK/ABI
741 * @param versionName Version prefix used for versioned files, e.g. ABI99_0_0
742 * @param versionedPodNames mapping from pod names to versioned pod names, e.g. React -> ReactABI99_0_0
743 * @param versionedReactPodPath path of the new react pod
744 */
745async function generatePodfileSubscriptsAsync(
746  versionNumber: string,
747  versionName: string,
748  versionedPodNames: Record<string, string>,
749  versionedReactPodPath: string
750) {
751  if (!versionedPodNames.React) {
752    throw new Error(
753      'Tried to add generate pod dependencies, but missing a name for the versioned library.'
754    );
755  }
756
757  const relativeReactNativePath = path.relative(IOS_DIR, getVersionedReactNativePath(versionName));
758  const relativeExpoKitPath = path.relative(IOS_DIR, getVersionedExpoKitPath(versionName));
759
760  // Add a dependency on newPodName
761  const dependenciesContent = `# @generated by expotools
762
763require './${relativeReactNativePath}/scripts/react_native_pods.rb'
764
765use_react_native_${versionName}!(
766  :path => './${relativeReactNativePath}',
767  :hermes_enabled => true,
768  :fabric_enabled => false,
769)
770setup_jsc_${versionName}!(
771  :react_native_path => './${relativeReactNativePath}',
772  :fabric_enabled => false,
773)
774
775pod '${getVersionedExpoKitPodName(versionName)}',
776  :path => './${relativeExpoKitPath}',
777  :project_name => '${versionName}',
778  :subspecs => ['Expo', 'ExpoOptional']
779
780use_pods! '{versioned,vendored}/sdk${semver.major(
781    versionNumber
782  )}/**/*.podspec.json', '${versionName}'
783`;
784
785  await fs.writeFile(path.join(versionedReactPodPath, 'dependencies.rb'), dependenciesContent);
786
787  // Add postinstall.
788  // In particular, resolve conflicting globals from React by redefining them.
789  const globals = {
790    React: [
791      // RCTNavigator
792      'kNeverRequested',
793      'kNeverProgressed',
794      // react-native-maps
795      'kSMCalloutViewRepositionDelayForUIScrollView',
796      'regionAsJSON',
797      'unionRect',
798      // jschelpers
799      'JSNoBytecodeFileFormatVersion',
800      'JSSamplingProfilerEnabled',
801      // RCTInspectorPackagerConnection
802      'RECONNECT_DELAY_MS',
803      // RCTSpringAnimation
804      'MAX_DELTA_TIME',
805    ],
806    yoga: [
807      'gCurrentGenerationCount',
808      'gPrintSkips',
809      'gPrintChanges',
810      'layoutNodeInternal',
811      'gDepth',
812      'gPrintTree',
813      'isUndefined',
814      'gNodeInstanceCount',
815    ],
816  };
817  const configValues = getCFlagsToPrefixGlobals(
818    versionedPodNames.React,
819    globals.React.concat(globals.yoga)
820  );
821  const indent = '  '.repeat(3);
822  const config = `# @generated by expotools
823
824if pod_name.start_with?('${versionedPodNames.React}') || pod_name == '${versionedPodNames.ExpoKit}'
825  target_installation_result.native_target.build_configurations.each do |config|
826    config.build_settings['OTHER_CFLAGS'] = %w[
827      ${configValues.join(`\n${indent}`)}
828      -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}React-Core/${versionName}React/${versionName}React-Core.modulemap"
829      -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}ExpoModulesCore/${versionName}ExpoModulesCore.modulemap"
830      -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}EXUpdates/${versionName}EXUpdates.modulemap"
831      -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}EXUpdatesInterface/${versionName}EXUpdatesInterface.modulemap"
832    ]
833    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)']
834    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV=1'
835    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_ENABLE_INSPECTOR=0'
836    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_REMOTE_PROFILE=0'
837    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION=0'
838    # Enable Google Maps support
839    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS=1'
840    config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS_UTILS=1'
841  end
842end
843`;
844  await fs.writeFile(path.join(versionedReactPodPath, 'postinstalls.rb'), config);
845}
846
847/**
848 * @param transformConfig function that takes a config dict and returns a new config dict.
849 */
850async function modifyVersionConfigAsync(configPath, transformConfig) {
851  const jsConfigFilename = `${configPath}/sdkVersions.json`;
852  await _transformFileContentsAsync(jsConfigFilename, (jsConfigContents) => {
853    let jsConfig;
854
855    // read the existing json config and add the new version to the sdkVersions array
856    try {
857      jsConfig = JSON.parse(jsConfigContents);
858    } catch (e) {
859      console.log('Error parsing existing sdkVersions.json file, writing a new one...', e);
860      console.log('The erroneous file contents was:', jsConfigContents);
861      jsConfig = {
862        sdkVersions: [],
863      };
864    }
865    // apply changes
866    jsConfig = transformConfig(jsConfig);
867    return JSON.stringify(jsConfig);
868  });
869
870  // convert json config to plist for iOS
871  await spawnAsync('plutil', [
872    '-convert',
873    'xml1',
874    jsConfigFilename,
875    '-o',
876    path.join(configPath, 'EXSDKVersions.plist'),
877  ]);
878}
879
880function validateAddVersionDirectories(rootPath, newVersionPath) {
881  // Make sure the paths we want to read are available
882  const relativePathsToCheck = [
883    RELATIVE_RN_PATH,
884    'ios/versioned-react-native',
885    'ios/Exponent',
886    'ios/Exponent/Versioned',
887  ];
888  let isValid = true;
889  relativePathsToCheck.forEach((path) => {
890    try {
891      fs.accessSync(`${rootPath}/${path}`, fs.constants.F_OK);
892    } catch {
893      console.log(`${rootPath}/${path} does not exist or is otherwise inaccessible`);
894      isValid = false;
895    }
896  });
897  // Also, make sure the version we're about to write doesn't already exist
898  try {
899    // we want this to fail
900    fs.accessSync(newVersionPath, fs.constants.F_OK);
901    console.log(`${newVersionPath} already exists, will not overwrite`);
902    isValid = false;
903  } catch {}
904
905  return isValid;
906}
907
908function validateRemoveVersionDirectories(rootPath, newVersionPath) {
909  const pathsToCheck = [
910    `${rootPath}/ios/versioned-react-native`,
911    `${rootPath}/ios/Exponent`,
912    newVersionPath,
913  ];
914  let isValid = true;
915  pathsToCheck.forEach((path) => {
916    try {
917      fs.accessSync(path, fs.constants.F_OK);
918    } catch {
919      console.log(`${path} does not exist or is otherwise inaccessible`);
920      isValid = false;
921    }
922  });
923  return isValid;
924}
925
926async function getConfigsFromArguments(versionNumber) {
927  let versionComponents = versionNumber.split('.');
928  versionComponents = versionComponents.map((number) => parseInt(number, 10));
929  const versionName = 'ABI' + versionNumber.replace(/\./g, '_');
930  const rootPathComponents = EXPO_DIR.split('/');
931  const versionPathComponents = path.join('ios', 'versioned-react-native', versionName).split('/');
932  const newVersionPath = rootPathComponents.concat(versionPathComponents).join('/');
933
934  const versionedPodNames = {
935    React: getVersionedReactPodName(versionName),
936    yoga: getVersionedYogaPodName(versionName),
937    ExpoKit: getVersionedExpoKitPodName(versionName),
938    jsireact: getVersionedJSIPodName(versionName),
939  };
940
941  return {
942    sdkNumber: semver.major(versionNumber),
943    versionName,
944    newVersionPath,
945    versionedPodNames,
946    versionComponents,
947  };
948}
949
950async function getVersionedUnimodulePodsAsync(
951  versionName: string
952): Promise<{ [key: string]: string }> {
953  const versionedUnimodulePods = {};
954  const packages = await getListOfPackagesAsync();
955
956  packages.forEach((pkg) => {
957    const podName = pkg.podspecName;
958    if (podName && pkg.isVersionableOnPlatform('ios')) {
959      versionedUnimodulePods[podName] = `${versionName}${podName}`;
960    }
961  });
962
963  return versionedUnimodulePods;
964}
965
966function getVersionedReactPodName(versionName: string): string {
967  return getVersionedLibraryName('React', versionName);
968}
969
970function getVersionedYogaPodName(versionName: string): string {
971  return getVersionedLibraryName('Yoga', versionName);
972}
973
974function getVersionedJSIPodName(versionName: string): string {
975  return getVersionedLibraryName('jsiReact', versionName);
976}
977
978function getVersionedExpoKitPodName(versionName: string): string {
979  return getVersionedLibraryName('ExpoKit', versionName);
980}
981
982function getVersionedLibraryName(libraryName: string, versionName: string): string {
983  return `${versionName}${libraryName}`;
984}
985
986function getVersionedReactNativePath(versionName: string): string {
987  return path.join(VERSIONED_RN_IOS_DIR, versionName, 'ReactNative');
988}
989
990function getVersionedExpoPath(versionName: string): string {
991  return path.join(VERSIONED_RN_IOS_DIR, versionName, 'Expo');
992}
993
994function getCppLibrariesToVersion() {
995  return [
996    {
997      libName: 'cxxreact',
998    },
999    {
1000      libName: 'jsi',
1001    },
1002    {
1003      libName: 'jsiexecutor',
1004      customHeaderDir: 'jsireact',
1005    },
1006    {
1007      libName: 'jsinspector',
1008    },
1009    {
1010      libName: 'yoga',
1011    },
1012    {
1013      libName: 'react',
1014    },
1015    {
1016      libName: 'callinvoker',
1017      customHeaderDir: 'ReactCommon',
1018    },
1019    {
1020      libName: 'reactperflogger',
1021    },
1022    {
1023      libName: 'runtimeexecutor',
1024    },
1025    {
1026      libName: 'logger',
1027    },
1028    {
1029      libName: 'hermes',
1030    },
1031    {
1032      libName: 'jsc',
1033    },
1034    {
1035      libName: 'butter',
1036    },
1037  ];
1038}
1039
1040export async function addVersionAsync(versionNumber: string, packages: Package[]) {
1041  const { sdkNumber, versionName, newVersionPath, versionedPodNames } =
1042    await getConfigsFromArguments(versionNumber);
1043
1044  // Validate the directories we need before doing anything
1045  console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`);
1046  const isFilesystemReady = validateAddVersionDirectories(EXPO_DIR, newVersionPath);
1047  if (!isFilesystemReady) {
1048    throw new Error('Aborting: At least one directory we need is not available');
1049  }
1050
1051  if (!versionedPodNames.React) {
1052    throw new Error('Missing name for versioned pod dependency.');
1053  }
1054
1055  // Create ABIXX_0_0 directory.
1056  console.log(
1057    `Creating new ABI version ${chalk.cyan(versionNumber)} at ${chalk.magenta(
1058      path.relative(EXPO_DIR, newVersionPath)
1059    )}`
1060  );
1061  await fs.mkdirs(newVersionPath);
1062
1063  // Generate new Podspec from the existing React.podspec
1064  console.log('Generating versioned ReactNative directory...');
1065  await generateVersionedReactNativeAsync(versionName);
1066
1067  console.log(
1068    `Generating ${chalk.magenta(
1069      path.relative(EXPO_DIR, getVersionedExpoPath(versionName))
1070    )} directory...`
1071  );
1072  await generateVersionedExpoAsync(versionName, sdkNumber);
1073
1074  await versionExpoModulesAsync(sdkNumber, packages);
1075
1076  // Generate versioned Swift modules provider
1077  await versionExpoModulesProviderAsync(sdkNumber);
1078
1079  // Namespace the new React clone
1080  console.log('Namespacing/transforming files...');
1081  await transformReactNativeAsync(newVersionPath, versionName, versionedPodNames);
1082
1083  // Generate Ruby scripts with versioned dependencies and postinstall actions that will be evaluated in the Expo client's Podfile.
1084  console.log('Adding dependency to root Podfile...');
1085  await generatePodfileSubscriptsAsync(
1086    versionNumber,
1087    versionName,
1088    versionedPodNames,
1089    newVersionPath
1090  );
1091
1092  // Add the new version to the iOS config list of available versions
1093  console.log('Registering new version under sdkVersions config...');
1094  const addVersionToConfig = (config, versionNumber) => {
1095    config.sdkVersions.push(versionNumber);
1096    return config;
1097  };
1098  await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) =>
1099    addVersionToConfig(config, versionNumber)
1100  );
1101  await modifyVersionConfigAsync(
1102    path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'),
1103    (config) => addVersionToConfig(config, versionNumber)
1104  );
1105
1106  // Modifying kernel files
1107  console.log(`Modifying ${chalk.bold('kernel files')} to incorporate new SDK version...`);
1108  await modifyKernelFilesAsync(versionName);
1109
1110  console.log('Removing any `filename--` files from the new pod ...');
1111
1112  try {
1113    const minusMinusFiles = [
1114      ...(await glob(path.join(newVersionPath, '**', '*--'))),
1115      ...(await glob(path.join(IOS_DIR, 'build', versionName, 'generated', 'ios', '**', '*--'))),
1116    ];
1117    for (const minusMinusFile of minusMinusFiles) {
1118      await fs.remove(minusMinusFile);
1119    }
1120  } catch {
1121    console.warn(
1122      "The script wasn't able to remove any possible `filename--` files created by sed. Please ensure there are no such files manually."
1123    );
1124  }
1125
1126  logger.info('\n�� Starting to build versioned Hermes tarball');
1127  const versionedReactNativeRoot = getVersionedReactNativePath(versionName);
1128  const hermesTarball = await createVersionedHermesTarball(versionedReactNativeRoot, versionName, {
1129    verbose: true,
1130  });
1131  await spawnAsync('tar', ['xfz', hermesTarball], {
1132    cwd: path.join(versionedReactNativeRoot, 'sdks', 'hermes-engine'),
1133  });
1134
1135  console.log('Finished creating new version.');
1136
1137  console.log(
1138    '\n' +
1139      chalk.yellow(
1140        '################################################################################################################'
1141      ) +
1142      `\nIf you want to commit the versioned code to git, please also upload the versioned Hermes tarball at ${chalk.cyan(
1143        hermesTarball
1144      )} to:\n` +
1145      chalk.cyan(
1146        `https://github.com/expo/react-native/releases/download/sdk-${sdkNumber}.0.0/${versionName}hermes.tar.gz`
1147      ) +
1148      '\n' +
1149      chalk.yellow(
1150        '################################################################################################################'
1151      ) +
1152      '\n'
1153  );
1154}
1155
1156async function askToReinstallPodsAsync(): Promise<boolean> {
1157  if (process.env.CI) {
1158    // If we're on the CI, let's regenerate Pods by default.
1159    return true;
1160  }
1161  const { result } = await inquirer.prompt<{ result: boolean }>([
1162    {
1163      type: 'confirm',
1164      name: 'result',
1165      message: 'Do you want to reinstall pods?',
1166      default: true,
1167    },
1168  ]);
1169  return result;
1170}
1171
1172export async function reinstallPodsAsync(force?: boolean, preventReinstall?: boolean) {
1173  if (
1174    preventReinstall !== true &&
1175    (force || (force !== false && (await askToReinstallPodsAsync())))
1176  ) {
1177    await spawnAsync('pod', ['install'], { stdio: 'inherit', cwd: IOS_DIR });
1178    console.log(
1179      'Regenerated Podfile and installed new pods. You can now try to build the project in Xcode.'
1180    );
1181  } else {
1182    console.log(
1183      'Skipped pods regeneration. You might want to run `et ios-generate-dynamic-macros`, then `pod install` in `ios` to configure Xcode project.'
1184    );
1185  }
1186}
1187
1188export async function removeVersionAsync(versionNumber: string) {
1189  const { sdkNumber, newVersionPath, versionedPodNames, versionName } =
1190    await getConfigsFromArguments(versionNumber);
1191
1192  console.log(
1193    `Removing SDK version ${chalk.cyan(versionNumber)} from ${chalk.magenta(
1194      path.relative(EXPO_DIR, newVersionPath)
1195    )} with Pod name ${chalk.green(versionedPodNames.React)}`
1196  );
1197
1198  // Validate the directories we need before doing anything
1199  console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`);
1200  const isFilesystemReady = validateRemoveVersionDirectories(EXPO_DIR, newVersionPath);
1201  if (!isFilesystemReady) {
1202    console.log('Aborting: At least one directory we expect is not available');
1203    return;
1204  }
1205
1206  // remove directory
1207  console.log(
1208    `Removing versioned files under ${chalk.magenta(path.relative(EXPO_DIR, newVersionPath))}...`
1209  );
1210  await fs.remove(newVersionPath);
1211  await fs.remove(getVersionedDirectory(sdkNumber));
1212
1213  console.log('Removing vendored libraries...');
1214  await removeVersionedVendoredModulesAsync(semver.major(versionNumber));
1215
1216  // remove dep from main podfile
1217  console.log(`Removing ${chalk.green(versionedPodNames.React)} dependency from root Podfile...`);
1218
1219  // remove from sdkVersions.json
1220  console.log('Unregistering version from sdkVersions config...');
1221  const removeVersionFromConfig = (config, versionNumber) => {
1222    const index = config.sdkVersions.indexOf(versionNumber);
1223    if (index > -1) {
1224      // modify in place
1225      config.sdkVersions.splice(index, 1);
1226    }
1227    return config;
1228  };
1229  await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) =>
1230    removeVersionFromConfig(config, versionNumber)
1231  );
1232  await modifyVersionConfigAsync(
1233    path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'),
1234    (config) => removeVersionFromConfig(config, versionNumber)
1235  );
1236
1237  // modify kernel files
1238  console.log('Rollbacking SDK modifications from kernel files...');
1239  await modifyKernelFilesAsync(versionName, true);
1240
1241  // Update `ios/ExpoKit.podspec` with the newest SDK version
1242  logger.info('�� Updating ExpoKit podspec');
1243  await renderExpoKitPodspecAsync(EXPO_DIR, path.join(EXPO_DIR, 'template-files'));
1244
1245  await reinstallPodsAsync();
1246}
1247
1248/**
1249 *  @return an array of objects representing react native transform rules.
1250 *    objects must contain 'pattern' and may optionally contain 'paths' to limit
1251 *    the transform to certain file paths.
1252 *
1253 *  the rules are applied in order!
1254 */
1255function _getReactNativeTransformRules(versionPrefix, reactPodName) {
1256  const cppLibraries = getCppLibrariesToVersion().map((lib) => lib.customHeaderDir || lib.libName);
1257  const versionedLibs = [...cppLibraries, 'React', 'FBLazyVector', 'FBReactNativeSpec'];
1258
1259  return [
1260    {
1261      // Change Obj-C symbols prefix
1262      pattern: `s/RCT/${versionPrefix}RCT/g`,
1263    },
1264    {
1265      pattern: `s/^EX/${versionPrefix}EX/g`,
1266      // paths: 'EX',
1267    },
1268    {
1269      pattern: `s/^UM/${versionPrefix}UM/g`,
1270      // paths: 'EX',
1271    },
1272    {
1273      pattern: `s/\\([^\\<\\/"]\\)YG/\\1${versionPrefix}YG/g`,
1274    },
1275    {
1276      pattern: `s/\\([\\<,]\\)YG/\\1${versionPrefix}YG/g`,
1277    },
1278    {
1279      pattern: `s/^YG/${versionPrefix}YG/g`,
1280    },
1281    {
1282      paths: 'Components',
1283      pattern: `s/\\([^+]\\)AIR/\\1${versionPrefix}AIR/g`,
1284    },
1285    {
1286      flags: '-Ei',
1287      pattern: `s/(^|[^A-Za-z0-9_+])(RN|REA|EX|UM|ART|SM)/\\1${versionPrefix}\\2/g`,
1288    },
1289    {
1290      paths: 'Core/Api',
1291      pattern: `s/^RN/${versionPrefix}RN/g`,
1292    },
1293    {
1294      paths: 'Core/Api',
1295      pattern: `s/HAVE_GOOGLE_MAPS/${versionPrefix}HAVE_GOOGLE_MAPS/g`,
1296    },
1297    {
1298      paths: 'Core/Api',
1299      pattern: `s/#import "Branch/#import "${versionPrefix}Branch/g`,
1300    },
1301    {
1302      paths: 'Core/Api',
1303      pattern: `s/#import "NSObject+RNBranch/#import "${versionPrefix}NSObject+RNBranch/g`,
1304    },
1305    {
1306      // React will be prefixed in a moment
1307      pattern: `s/#import <${versionPrefix}RCTAnimation/#import <React/g`,
1308    },
1309    {
1310      pattern: `s/^REA/${versionPrefix}REA/g`,
1311      paths: 'Core/Api/Reanimated',
1312    },
1313    {
1314      // Prefixes all direct references to objects under `reanimated` namespace.
1315      // It must be applied before versioning `namespace reanimated` so
1316      // `using namespace reanimated::` don't get versioned twice.
1317      pattern: `s/reanimated::/${versionPrefix}reanimated::/g`,
1318    },
1319    {
1320      // Prefixes reanimated namespace.
1321      pattern: `s/namespace reanimated/namespace ${versionPrefix}reanimated/g`,
1322    },
1323    {
1324      // Fix imports in C++ libs in ReactCommon.
1325      // Extended syntax (-E) is required to use (a|b).
1326      flags: '-Ei',
1327      pattern: `s/([<"])(${versionedLibs.join(
1328        '|'
1329      )})\\//\\1${versionPrefix}\\2\\/${versionPrefix}/g`,
1330    },
1331    {
1332      // Change React -> new pod name
1333      // e.g. threads and queues namespaced to com.facebook.react,
1334      // file paths beginning with the lib name,
1335      // the cpp facebook::react namespace,
1336      // iOS categories ending in +React
1337      flags: '-Ei',
1338      pattern: `s/[Rr]eact/${reactPodName}/g`,
1339    },
1340    {
1341      // Imports from cxxreact and jsireact got prefixed twice.
1342      flags: '-Ei',
1343      pattern: `s/([<"])(${versionPrefix})(cxx|jsi)${versionPrefix}React/\\1\\2\\3react/g`,
1344    },
1345    {
1346      // Fix imports from files like `UIView+React.*`.
1347      flags: '-Ei',
1348      pattern: `s/\\+${versionPrefix}React/\\+React/g`,
1349    },
1350    {
1351      // Prefixes all direct references to objects under `facebook` and `JS` namespaces.
1352      // It must be applied before versioning `namespace facebook` so
1353      // `using namespace facebook::` don't get versioned twice.
1354      flags: '-Ei',
1355      pattern: `s/(facebook|JS|hermes)::/${versionPrefix}\\1::/g`,
1356    },
1357    {
1358      // Prefixes facebook namespace.
1359      flags: '-Ei',
1360      pattern: `s/namespace (facebook|JS|hermes)/namespace ${versionPrefix}\\1/g`,
1361    },
1362    {
1363      // Prefixes for `namespace h = ::facebook::hermes;`
1364      flags: '-Ei',
1365      pattern: `s/namespace (.+::)(hermes)/namespace \\1${versionPrefix}\\2/g`,
1366    },
1367    {
1368      // For UMReactNativeAdapter
1369      // Fix names with 'React' substring occurring twice - only first one should be prefixed
1370      flags: '-Ei',
1371      pattern: `s/${versionPrefix}UM([[:alpha:]]*)${reactPodName}/${versionPrefix}UM\\1React/g`,
1372    },
1373    {
1374      // For EXReactNativeAdapter
1375      pattern: `s/${versionPrefix}EX${reactPodName}/${versionPrefix}EXReact/g`,
1376    },
1377    {
1378      // For EXConstants and EXNotifications so that when their migrators
1379      // try to access legacy storage for UUID migration, they access the proper value.
1380      pattern: `s/${versionPrefix}EXDeviceInstallUUIDKey/EXDeviceInstallUUIDKey/g`,
1381      paths: 'Expo',
1382    },
1383    {
1384      // For EXConstants and EXNotifications so that the installation ID
1385      // stays the same between different SDK versions. (https://github.com/expo/expo/issues/11008#issuecomment-726370187)
1386      pattern: `s/${versionPrefix}EXDeviceInstallationUUIDKey/EXDeviceInstallationUUIDKey/g`,
1387      paths: 'Expo',
1388    },
1389    {
1390      // RCTPlatform exports version of React Native
1391      pattern: `s/${reactPodName}NativeVersion/reactNativeVersion/g`,
1392    },
1393    {
1394      pattern: `s/@"${versionPrefix}RCT"/@"RCT"/g`,
1395    },
1396    {
1397      // Unprefix everything that got prefixed twice or more times.
1398      flags: '-Ei',
1399      pattern: `s/(${versionPrefix}){2,}/\\1/g`,
1400    },
1401    {
1402      flags: '-Ei',
1403      pattern: `s/(#import |__has_include\\()<(Expo|RNReanimated)/\\1<${versionPrefix}\\2/g`,
1404    },
1405    {
1406      // Unprefix jsc dirname in JSCExecutorFactory.mm
1407      paths: 'CxxBridge',
1408      flags: '-Ei',
1409      pattern: `s/(#import )<${versionPrefix}jsc\\/${versionPrefix}JSCRuntime\.h>/\\1<jsc\\/${versionPrefix}JSCRuntime.h>/g`,
1410    },
1411  ];
1412}
1413
1414function _getTransformRulesForDirname(transformRules, dirname) {
1415  return transformRules.filter((rule) => {
1416    return (
1417      // no paths specified, so apply rule to everything
1418      !rule.paths ||
1419      // otherwise, limit this rule to paths specified
1420      dirname.indexOf(rule.paths) !== -1
1421    );
1422  });
1423}
1424
1425// TODO: use the one in XDL
1426function _isDirectory(dir) {
1427  try {
1428    if (fs.statSync(dir).isDirectory()) {
1429      return true;
1430    }
1431
1432    return false;
1433  } catch {
1434    return false;
1435  }
1436}
1437
1438// TODO: use the one in XDL
1439async function _transformFileContentsAsync(
1440  filename: string,
1441  transform: (fileString: string) => Promise<string> | string | null
1442) {
1443  const fileString = await fs.readFile(filename, 'utf8');
1444  const newFileString = await transform(fileString);
1445  if (newFileString !== null) {
1446    await fs.writeFile(filename, newFileString);
1447  }
1448}
1449