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