xref: /expo/tools/src/versioning/android/index.ts (revision feddcedd)
1import spawnAsync from '@expo/spawn-async';
2import chalk from 'chalk';
3import fs from 'fs-extra';
4import glob from 'glob-promise';
5import inquirer from 'inquirer';
6import path from 'path';
7import semver from 'semver';
8
9import * as Directories from '../../Directories';
10import { getListOfPackagesAsync } from '../../Packages';
11import { JniLibNames, getJavaPackagesToRename } from './libraries';
12import { renameHermesEngine, updateVersionedReactNativeAsync } from './versionReactNative';
13
14const EXPO_DIR = Directories.getExpoRepositoryRootDir();
15const ANDROID_DIR = Directories.getAndroidDir();
16const EXPOTOOLS_DIR = Directories.getExpotoolsDir();
17const SCRIPT_DIR = path.join(EXPOTOOLS_DIR, 'src/versioning/android');
18
19const appPath = path.join(ANDROID_DIR, 'app');
20const expoviewPath = path.join(ANDROID_DIR, 'expoview');
21const versionedAbisPath = path.join(ANDROID_DIR, 'versioned-abis');
22const versionedExpoviewAbiPath = (abiName) => path.join(versionedAbisPath, `expoview-${abiName}`);
23const expoviewBuildGradlePath = path.join(expoviewPath, 'build.gradle');
24const appManifestPath = path.join(appPath, 'src', 'main', 'AndroidManifest.xml');
25const templateManifestPath = path.join(
26  EXPO_DIR,
27  'template-files',
28  'android',
29  'AndroidManifest.xml'
30);
31const settingsGradlePath = path.join(ANDROID_DIR, 'settings.gradle');
32const appBuildGradlePath = path.join(appPath, 'build.gradle');
33const buildGradlePath = path.join(ANDROID_DIR, 'build.gradle');
34const sdkVersionsPath = path.join(ANDROID_DIR, 'sdkVersions.json');
35const rnActivityPath = path.join(
36  expoviewPath,
37  'src/versioned/java/host/exp/exponent/experience/MultipleVersionReactNativeActivity.java'
38);
39const expoviewConstantsPath = path.join(
40  expoviewPath,
41  'src/main/java/host/exp/exponent/Constants.java'
42);
43const testSuiteTestsPath = path.join(
44  appPath,
45  'src/androidTest/java/host/exp/exponent/TestSuiteTests.java'
46);
47const versionedReactAndroidPath = path.join(ANDROID_DIR, 'versioned-react-native/ReactAndroid');
48const versionedReactAndroidJniPath = path.join(versionedReactAndroidPath, 'src/main');
49const versionedReactAndroidJavaPath = path.join(versionedReactAndroidJniPath, 'java');
50const versionedReactCommonPath = path.join(ANDROID_DIR, 'versioned-react-native/ReactCommon');
51
52async function transformFileAsync(filePath: string, regexp: RegExp, replacement: string = '') {
53  const fileContent = await fs.readFile(filePath, 'utf8');
54  await fs.writeFile(filePath, fileContent.replace(regexp, replacement));
55}
56
57async function removeVersionReferencesFromFileAsync(sdkMajorVersion: string, filePath: string) {
58  console.log(
59    `Removing code surrounded by ${chalk.gray(`// BEGIN_SDK_${sdkMajorVersion}`)} and ${chalk.gray(
60      `// END_SDK_${sdkMajorVersion}`
61    )} from ${chalk.magenta(path.relative(EXPO_DIR, filePath))}...`
62  );
63  await transformFileAsync(
64    filePath,
65    new RegExp(
66      `\\s*//\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\n.*?//\\s*END_SDK_${sdkMajorVersion}(_\d+)*`,
67      'gs'
68    ),
69    ''
70  );
71}
72
73async function removeVersionedExpoviewAsync(versionedExpoviewAbiPath: string) {
74  console.log(
75    `Removing versioned expoview at ${chalk.magenta(
76      path.relative(EXPO_DIR, versionedExpoviewAbiPath)
77    )}...`
78  );
79  await fs.remove(versionedExpoviewAbiPath);
80}
81
82async function removeFromManifestAsync(sdkMajorVersion: string, manifestPath: string) {
83  console.log(
84    `Removing code surrounded by ${chalk.gray(
85      `<!-- BEGIN_SDK_${sdkMajorVersion} -->`
86    )} and ${chalk.gray(`<!-- END_SDK_${sdkMajorVersion} -->`)} from ${chalk.magenta(
87      path.relative(EXPO_DIR, manifestPath)
88    )}...`
89  );
90  await transformFileAsync(
91    manifestPath,
92    new RegExp(
93      `\\s*<!--\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\s*-->.*?<!--\\s*END_SDK_${sdkMajorVersion}(_\d+)*\\s*-->`,
94      'gs'
95    ),
96    ''
97  );
98}
99
100async function removeFromSettingsGradleAsync(abiName: string, settingsGradlePath: string) {
101  console.log(
102    `Removing ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta(
103      path.relative(EXPO_DIR, settingsGradlePath)
104    )}...`
105  );
106  await transformFileAsync(settingsGradlePath, new RegExp(`\\n\\s*"${abiName}",[^\\n]*`, 'g'), '');
107}
108
109async function removeFromBuildGradleAsync(abiName: string, buildGradlePath: string) {
110  console.log(
111    `Removing maven repository for ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta(
112      path.relative(EXPO_DIR, buildGradlePath)
113    )}...`
114  );
115  await transformFileAsync(
116    buildGradlePath,
117    new RegExp(`\\s*maven\\s*{\\s*url\\s*".*?/expoview-${abiName}/maven"\\s*}[^\\n]*`),
118    ''
119  );
120}
121
122async function removeFromSdkVersionsAsync(version: string, sdkVersionsPath: string) {
123  console.log(
124    `Removing ${chalk.cyan(version)} from ${chalk.magenta(
125      path.relative(EXPO_DIR, sdkVersionsPath)
126    )}...`
127  );
128  await transformFileAsync(sdkVersionsPath, new RegExp(`"${version}",\s*`, 'g'), '');
129}
130
131async function removeTestSuiteTestsAsync(version: string, testsFilePath: string) {
132  console.log(
133    `Removing test-suite tests from ${chalk.magenta(path.relative(EXPO_DIR, testsFilePath))}...`
134  );
135  await transformFileAsync(
136    testsFilePath,
137    new RegExp(`\\s*(@\\w+\\s+)*@ExpoSdkVersionTest\\("${version}"\\)[^}]+}`),
138    ''
139  );
140}
141
142async function findAndPrintVersionReferencesInSourceFilesAsync(version: string): Promise<boolean> {
143  const pattern = new RegExp(
144    `(${version.replace(/\./g, '[._]')}|(SDK|ABI).?${semver.major(version)})`,
145    'ig'
146  );
147  let matchesCount = 0;
148
149  const files = await glob('**/{src/**/*.@(java|kt|xml),build.gradle}', { cwd: ANDROID_DIR });
150
151  for (const file of files) {
152    const filePath = path.join(ANDROID_DIR, file);
153    const fileContent = await fs.readFile(filePath, 'utf8');
154    const fileLines = fileContent.split(/\r\n?|\n/g);
155    let match;
156
157    while ((match = pattern.exec(fileContent)) != null) {
158      const index = pattern.lastIndex - match[0].length;
159      const lineNumberWithMatch = fileContent.substring(0, index).split(/\r\n?|\n/g).length - 1;
160      const firstLineInContext = Math.max(0, lineNumberWithMatch - 2);
161      const lastLineInContext = Math.min(lineNumberWithMatch + 2, fileLines.length);
162
163      ++matchesCount;
164
165      console.log(
166        `Found ${chalk.bold.green(match[0])} in ${chalk.magenta(
167          path.relative(EXPO_DIR, filePath)
168        )}:`
169      );
170
171      for (let lineIndex = firstLineInContext; lineIndex <= lastLineInContext; lineIndex++) {
172        console.log(
173          `${chalk.gray(1 + lineIndex + ':')} ${fileLines[lineIndex].replace(
174            match[0],
175            chalk.bgMagenta(match[0])
176          )}`
177        );
178      }
179      console.log();
180    }
181  }
182  return matchesCount > 0;
183}
184
185export async function removeVersionAsync(version: string) {
186  const abiName = `abi${version.replace(/\./g, '_')}`;
187  const sdkMajorVersion = `${semver.major(version)}`;
188
189  console.log(`Removing SDK version ${chalk.cyan(version)} for ${chalk.blue('Android')}...`);
190
191  // Remove expoview-abi*_0_0 library
192  await removeVersionedExpoviewAsync(versionedExpoviewAbiPath(abiName));
193  await removeFromSettingsGradleAsync(abiName, settingsGradlePath);
194  await removeFromBuildGradleAsync(abiName, buildGradlePath);
195
196  // Remove code surrounded by BEGIN_SDK_* and END_SDK_*
197  await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewBuildGradlePath);
198  await removeVersionReferencesFromFileAsync(sdkMajorVersion, appBuildGradlePath);
199  await removeVersionReferencesFromFileAsync(sdkMajorVersion, rnActivityPath);
200  await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewConstantsPath);
201
202  // Remove test-suite tests from the app.
203  await removeTestSuiteTestsAsync(version, testSuiteTestsPath);
204
205  // Update AndroidManifests
206  await removeFromManifestAsync(sdkMajorVersion, appManifestPath);
207  await removeFromManifestAsync(sdkMajorVersion, templateManifestPath);
208
209  // Remove SDK version from the list of supported SDKs
210  await removeFromSdkVersionsAsync(version, sdkVersionsPath);
211
212  console.log(`\nLooking for SDK references in source files...`);
213
214  if (await findAndPrintVersionReferencesInSourceFilesAsync(version)) {
215    console.log(
216      chalk.yellow(`Please review all of these references and remove them manually if possible!\n`)
217    );
218  }
219}
220
221function renameLib(lib: string, abiVersion: string) {
222  for (let i = 0; i < JniLibNames.length; i++) {
223    if (lib.endsWith(JniLibNames[i])) {
224      return `${lib}_abi${abiVersion}`;
225    }
226    if (lib.endsWith(`${JniLibNames[i]}.so`)) {
227      const { dir, name, ext } = path.parse(lib);
228      return path.join(dir, `${name}_abi${abiVersion}${ext}`);
229    }
230  }
231
232  return lib;
233}
234
235function processLine(line: string, abiVersion: string) {
236  if (
237    line.startsWith('LOCAL_MODULE') ||
238    line.startsWith('LOCAL_SHARED_LIBRARIES') ||
239    line.startsWith('LOCAL_STATIC_LIBRARIES') ||
240    line.startsWith('LOCAL_SRC_FILES')
241  ) {
242    let splitLine = line.split('=');
243    let libs = splitLine[1].split(' ');
244    for (let i = 0; i < libs.length; i++) {
245      libs[i] = renameLib(libs[i], abiVersion);
246    }
247    splitLine[1] = libs.join(' ');
248    line = splitLine.join('=');
249  }
250
251  return line;
252}
253
254async function processMkFileAsync(filename: string, abiVersion: string) {
255  let file = await fs.readFile(filename);
256  let fileString = file.toString();
257  await fs.truncate(filename, 0);
258  let lines = fileString.split('\n');
259  for (let i = 0; i < lines.length; i++) {
260    let line = lines[i];
261    line = processLine(line, abiVersion);
262    await fs.appendFile(filename, `${line}\n`);
263  }
264}
265
266async function processCMake(filePath: string, abiVersion: string) {
267  const libNameToReplace = new Set<string>();
268  for (const libName of JniLibNames) {
269    if (libName.startsWith('lib')) {
270      // in CMake we don't use the lib prefix
271      libNameToReplace.add(libName.slice(3));
272    } else {
273      libNameToReplace.add(libName);
274    }
275  }
276
277  libNameToReplace.delete('fb');
278  libNameToReplace.delete('fbjni'); // we use the prebuilt binary which is part of the `com.facebook.fbjni:fbjni`
279
280  for (const libName of libNameToReplace) {
281    await spawnAsync(
282      `sed -ri '' 's/${libName}([^\/]*$)/${libName}_abi${abiVersion}\\1/g' ${filePath}`,
283      [],
284      {
285        shell: true,
286      }
287    );
288  }
289}
290
291async function processJavaCodeAsync(libName: string, abiVersion: string) {
292  const abiName = `abi${abiVersion}`;
293  return spawnAsync(
294    `find ${versionedReactAndroidJavaPath} ${versionedExpoviewAbiPath(
295      abiName
296    )} -iname '*.java' -type f -print0 | ` +
297      `xargs -0 sed -i '' 's/"${libName}"/"${libName}_abi${abiVersion}"/g'`,
298    [],
299    { shell: true }
300  );
301}
302
303async function ensureToolsInstalledAsync() {
304  try {
305    await spawnAsync('patchelf', ['-h'], { ignoreStdio: true });
306  } catch (e) {
307    throw new Error('patchelf not found.');
308  }
309}
310
311async function renameJniLibsAsync(version: string) {
312  const abiVersion = version.replace(/\./g, '_');
313  const abiPrefix = `abi${abiVersion}`;
314  const versionedAbiPath = path.join(
315    Directories.getAndroidDir(),
316    'versioned-abis',
317    `expoview-${abiPrefix}`
318  );
319
320  // Update JNI methods
321  const packagesToRename = await getJavaPackagesToRename();
322  const codegenOutputRoot = path.join(ANDROID_DIR, 'versioned-react-native', 'codegen');
323  for (const javaPackage of packagesToRename) {
324    const pathForPackage = javaPackage.replace(/\./g, '\\/');
325    await spawnAsync(
326      `find ${versionedReactCommonPath} ${versionedReactAndroidJniPath} ${codegenOutputRoot} -type f ` +
327        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
328        `xargs -0 sed -i '' 's/${pathForPackage}/abi${abiVersion}\\/${pathForPackage}/g'`,
329      [],
330      { shell: true }
331    );
332
333    // reanimated
334    const oldJNIReanimatedPackage =
335      'versioned\\/host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
336    const newJNIReanimatedPackage = 'host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
337    await spawnAsync(
338      `find ${versionedAbiPath} -type f ` +
339        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
340        `xargs -0 sed -i '' 's/${oldJNIReanimatedPackage}/abi${abiVersion}\\/${newJNIReanimatedPackage}/g'`,
341      [],
342      { shell: true }
343    );
344  }
345
346  // Update LOCAL_MODULE, LOCAL_SHARED_LIBRARIES, LOCAL_STATIC_LIBRARIES fields in .mk files
347  let [
348    reactCommonMkFiles,
349    reactAndroidMkFiles,
350    versionedAbiMKFiles,
351    reactAndroidPrebuiltMk,
352    codegenMkFiles,
353  ] = await Promise.all([
354    glob(path.join(versionedReactCommonPath, '**/*.mk')),
355    glob(path.join(versionedReactAndroidJniPath, '**/*.mk')),
356    glob(path.join(versionedAbiPath, '**/*.mk')),
357    path.join(versionedReactAndroidPath, 'Android-prebuilt.mk'),
358    glob(path.join(codegenOutputRoot, '**/*.mk')),
359  ]);
360  let filenames = [
361    ...reactCommonMkFiles,
362    ...reactAndroidMkFiles,
363    ...versionedAbiMKFiles,
364    reactAndroidPrebuiltMk,
365    ...codegenMkFiles,
366  ];
367  await Promise.all(filenames.map((filename) => processMkFileAsync(filename, abiVersion)));
368
369  // Rename references to JNI libs in CMake
370  const cmakesFiles = await glob(path.join(versionedAbiPath, '**/CMakeLists.txt'));
371  await Promise.all(cmakesFiles.map((file) => processCMake(file, abiVersion)));
372
373  // Rename references to JNI libs in Java code
374  for (let i = 0; i < JniLibNames.length; i++) {
375    let libName = JniLibNames[i];
376    await processJavaCodeAsync(libName, abiVersion);
377  }
378
379  // 'fbjni' is loaded without the 'lib' prefix in com.facebook.jni.Prerequisites
380  await processJavaCodeAsync('fbjni', abiVersion);
381  await processJavaCodeAsync('fb', abiVersion);
382
383  console.log('\nThese are the JNI lib names we modified:');
384  await spawnAsync(
385    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "_abi${abiVersion}"`,
386    [],
387    { shell: true, stdio: 'inherit' }
388  );
389
390  console.log('\nAnd here are all instances of loadLibrary:');
391  await spawnAsync(
392    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "loadLibrary"`,
393    [],
394    { shell: true, stdio: 'inherit' }
395  );
396
397  const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([
398    {
399      type: 'confirm',
400      name: 'isCorrect',
401      message: 'Does all that look correct?',
402      default: false,
403    },
404  ]);
405  if (!isCorrect) {
406    throw new Error('Fix JNI libs');
407  }
408}
409
410async function copyUnimodulesAsync(version: string) {
411  const packages = await getListOfPackagesAsync();
412  for (const pkg of packages) {
413    if (
414      pkg.isSupportedOnPlatform('android') &&
415      pkg.isIncludedInExpoClientOnPlatform('android') &&
416      pkg.isVersionableOnPlatform('android')
417    ) {
418      await spawnAsync(
419        './android-copy-unimodule.sh',
420        [version, path.join(pkg.path, pkg.androidSubdirectory)],
421        {
422          shell: true,
423          cwd: SCRIPT_DIR,
424        }
425      );
426      console.log(`   ✅  Created versioned ${pkg.packageName}`);
427    }
428  }
429}
430
431async function addVersionedActivitesToManifests(version: string) {
432  const abiVersion = version.replace(/\./g, '_');
433  const abiName = `abi${abiVersion}`;
434  const majorVersion = semver.major(version);
435
436  await transformFileAsync(
437    templateManifestPath,
438    new RegExp('<!-- ADD DEV SETTINGS HERE -->'),
439    `<!-- ADD DEV SETTINGS HERE -->
440    <!-- BEGIN_SDK_${majorVersion} -->
441    <activity android:name="${abiName}.com.facebook.react.devsupport.DevSettingsActivity"/>
442    <!-- END_SDK_${majorVersion} -->`
443  );
444
445  await transformFileAsync(
446    templateManifestPath,
447    new RegExp('<!-- Versioned Activity for Stripe -->'),
448    `<!-- Versioned Activity for Stripe -->
449    <!-- BEGIN_SDK_${majorVersion} -->
450    <activity
451      android:exported="true"
452      android:launchMode="singleTask"
453      android:name="${abiName}.expo.modules.payments.stripe.RedirectUriReceiver"
454      android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
455      <intent-filter>
456        <action android:name="android.intent.action.VIEW" />
457        <category android:name="android.intent.category.DEFAULT" />
458        <category android:name="android.intent.category.BROWSABLE" />
459        <data android:scheme="${abiName}.expo.modules.payments.stripe" />
460      </intent-filter>
461    </activity>
462    <!-- END_SDK_${majorVersion} -->`
463  );
464}
465
466async function registerNewVersionUnderSdkVersions(version: string) {
467  let fileString = await fs.readFile(sdkVersionsPath, 'utf8');
468  let jsConfig;
469  // read the existing json config and add the new version to the sdkVersions array
470  try {
471    jsConfig = JSON.parse(fileString);
472  } catch (e) {
473    console.log('Error parsing existing sdkVersions.json file, writing a new one...', e);
474    console.log('The erroneous file contents was:', fileString);
475    jsConfig = {
476      sdkVersions: [],
477    };
478  }
479  // apply changes
480  jsConfig.sdkVersions.push(version);
481  await fs.writeFile(sdkVersionsPath, JSON.stringify(jsConfig));
482}
483
484async function cleanUpAsync(version: string) {
485  const abiVersion = version.replace(/\./g, '_');
486  const abiName = `abi${abiVersion}`;
487
488  const versionedAbiSrcPath = path.join(
489    versionedExpoviewAbiPath(abiName),
490    'src/main/java',
491    abiName
492  );
493
494  let filesToDelete: string[] = [];
495
496  // delete PrintDocumentAdapter*Callback.java
497  // their package is `android.print` and therefore they are not changed by the versioning script
498  // so we will have duplicate classes
499  const printCallbackFiles = await glob(
500    path.join(versionedAbiSrcPath, 'expo/modules/print/*Callback.java')
501  );
502  for (const file of printCallbackFiles) {
503    const contents = await fs.readFile(file, 'utf8');
504    if (!contents.includes(`package ${abiName}`)) {
505      filesToDelete.push(file);
506    } else {
507      console.log(`Skipping deleting ${file} because it appears to have been versioned`);
508    }
509  }
510
511  // delete versioned loader providers since we don't need them
512  filesToDelete.push(path.join(versionedAbiSrcPath, 'expo/loaders'));
513
514  console.log('Deleting the following files and directories:');
515  console.log(filesToDelete);
516
517  for (const file of filesToDelete) {
518    await fs.remove(file);
519  }
520
521  // misc fixes for versioned code
522  const versionedExponentPackagePath = path.join(
523    versionedAbiSrcPath,
524    'host/exp/exponent/ExponentPackage.java'
525  );
526  await transformFileAsync(
527    versionedExponentPackagePath,
528    new RegExp('// WHEN_VERSIONING_REMOVE_FROM_HERE', 'g'),
529    '/* WHEN_VERSIONING_REMOVE_FROM_HERE'
530  );
531  await transformFileAsync(
532    versionedExponentPackagePath,
533    new RegExp('// WHEN_VERSIONING_REMOVE_TO_HERE', 'g'),
534    'WHEN_VERSIONING_REMOVE_TO_HERE */'
535  );
536
537  await transformFileAsync(
538    path.join(versionedAbiSrcPath, 'host/exp/exponent/VersionedUtils.java'),
539    new RegExp('// DO NOT EDIT THIS COMMENT - used by versioning scripts[^,]+,[^,]+,'),
540    'null, null,'
541  );
542
543  // replace abixx_x_x...R with abixx_x_x.host.exp.expoview.R
544  await spawnAsync(
545    `find ${versionedAbiSrcPath} -iname '*.java' -type f -print0 | ` +
546      `xargs -0 sed -i '' 's/import ${abiName}\.[^;]*\.R;/import ${abiName}.host.exp.expoview.R;/g'`,
547    [],
548    { shell: true }
549  );
550  await spawnAsync(
551    `find ${versionedAbiSrcPath} -iname '*.kt' -type f -print0 | ` +
552      `xargs -0 sed -i '' 's/import ${abiName}\\..*\\.R$/import ${abiName}.host.exp.expoview.R/g'`,
553    [],
554    { shell: true }
555  );
556
557  // add new versioned maven to build.gradle
558  await transformFileAsync(
559    buildGradlePath,
560    new RegExp('// For old expoviews to work'),
561    `// For old expoviews to work
562    maven {
563      url "$rootDir/versioned-abis/expoview-${abiName}/maven"
564    }`
565  );
566}
567
568async function prepareReanimatedAsync(version: string): Promise<void> {
569  const abiVersion = version.replace(/\./g, '_');
570  const abiName = `abi${abiVersion}`;
571  const versionedExpoviewPath = versionedExpoviewAbiPath(abiName);
572
573  const buildReanimatedSO = async () => {
574    await spawnAsync(`./gradlew :expoview-${abiName}:packageNdkLibs`, [], {
575      shell: true,
576      cwd: path.join(versionedExpoviewPath, '../../'),
577      stdio: 'inherit',
578    });
579  };
580
581  const removeLeftoverDirectories = async () => {
582    const mainPath = path.join(versionedExpoviewPath, 'src', 'main');
583    const toRemove = ['Common', 'JNI', 'cpp'];
584    for (let dir of toRemove) {
585      await fs.remove(path.join(mainPath, dir));
586    }
587  };
588
589  const removeLeftoversFromGradle = async () => {
590    await spawnAsync('./android-remove-reanimated-code-from-gradle.sh', [version], {
591      shell: true,
592      cwd: SCRIPT_DIR,
593      stdio: 'inherit',
594    });
595  };
596
597  await buildReanimatedSO();
598  await removeLeftoverDirectories();
599  await removeLeftoversFromGradle();
600}
601
602async function exportReactNdks() {
603  const versionedRN = path.join(versionedReactAndroidPath, '..');
604  await spawnAsync(`./gradlew :ReactAndroid:packageReactNdkLibs`, [], {
605    shell: true,
606    cwd: versionedRN,
607    stdio: 'inherit',
608  });
609}
610
611async function exportReactNdksIfNeeded() {
612  const ndksPath = path.join(versionedReactAndroidPath, 'build', 'react-ndk', 'exported');
613  const exists = await fs.pathExists(ndksPath);
614  if (!exists) {
615    await exportReactNdks();
616    return;
617  }
618
619  const exportedSO = await glob(path.join(ndksPath, '**/*.so'));
620  if (exportedSO.length === 0) {
621    await exportReactNdks();
622  }
623}
624
625export async function addVersionAsync(version: string) {
626  await ensureToolsInstalledAsync();
627
628  console.log(' ��   1/11: Updating android/versioned-react-native...');
629  await updateVersionedReactNativeAsync(
630    Directories.getReactNativeSubmoduleDir(),
631    path.join(ANDROID_DIR, 'versioned-react-native')
632  );
633  console.log(' ✅  1/11: Finished\n\n');
634
635  console.log(' ��   2/11: Creating versioned expoview package...');
636  await spawnAsync('./android-copy-expoview.sh', [version], {
637    shell: true,
638    cwd: SCRIPT_DIR,
639  });
640
641  console.log(' ✅  2/11: Finished\n\n');
642
643  console.log(' ��   3/11: Renaming JNI libs in android/versioned-react-native and Reanimated...');
644  await renameJniLibsAsync(version);
645  console.log(' ✅  3/11: Finished\n\n');
646
647  console.log(' ��   4/11: Renaming libhermes.so...');
648  await renameHermesEngine(versionedReactAndroidPath, version);
649  console.log(' ✅  4/11: Finished\n\n');
650
651  console.log(' ��   5/11: Building versioned ReactAndroid AAR...');
652  await spawnAsync('./android-build-aar.sh', [version], {
653    shell: true,
654    cwd: SCRIPT_DIR,
655    stdio: 'inherit',
656  });
657  console.log(' ✅  5/11: Finished\n\n');
658
659  console.log(' ��   6/11: Exporting react ndks if needed...');
660  await exportReactNdksIfNeeded();
661  console.log(' ✅  6/11: Finished\n\n');
662
663  console.log(' ��   7/11: prepare versioned Reanimated...');
664  await prepareReanimatedAsync(version);
665  console.log(' ✅  7/11: Finished\n\n');
666
667  console.log(' ��   8/11: Creating versioned unimodule packages...');
668  await copyUnimodulesAsync(version);
669  console.log(' ✅  8/11: Finished\n\n');
670
671  console.log(' ��   9/11: Adding extra versioned activites to AndroidManifest...');
672  await addVersionedActivitesToManifests(version);
673  console.log(' ✅  9/11: Finished\n\n');
674
675  console.log(' ��   10/11: Registering new version under sdkVersions config...');
676  await registerNewVersionUnderSdkVersions(version);
677  console.log(' ✅  10/11: Finished\n\n');
678
679  console.log(' ��   11/11: Misc cleanup...');
680  await cleanUpAsync(version);
681  console.log(' ✅  11/11: Finished');
682}
683