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