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