xref: /expo/tools/src/versioning/android/index.ts (revision 4e758f24)
1import chalk from 'chalk';
2import fs from 'fs-extra';
3import glob from 'glob-promise';
4import inquirer from 'inquirer';
5import path from 'path';
6import semver from 'semver';
7import spawnAsync from '@expo/spawn-async';
8
9import { JniLibNames, getJavaPackagesToRename } from './libraries';
10import * as Directories from '../../Directories';
11import { getListOfPackagesAsync } from '../../Packages';
12
13const EXPO_DIR = Directories.getExpoRepositoryRootDir();
14const ANDROID_DIR = Directories.getAndroidDir();
15const EXPOTOOLS_DIR = Directories.getExpotoolsDir();
16const SCRIPT_DIR = path.join(EXPOTOOLS_DIR, 'src/versioning/android');
17
18const appPath = path.join(ANDROID_DIR, 'app');
19const expoviewPath = path.join(ANDROID_DIR, 'expoview');
20const versionedAbisPath = path.join(ANDROID_DIR, 'versioned-abis');
21const versionedExpoviewAbiPath = (abiName) => path.join(versionedAbisPath, `expoview-${abiName}`);
22const expoviewBuildGradlePath = path.join(expoviewPath, 'build.gradle');
23const appManifestPath = path.join(appPath, 'src', 'main', 'AndroidManifest.xml');
24const templateManifestPath = path.join(
25  EXPO_DIR,
26  'template-files',
27  'android',
28  'AndroidManifest.xml'
29);
30const settingsGradlePath = path.join(ANDROID_DIR, 'settings.gradle');
31const appBuildGradlePath = path.join(appPath, 'build.gradle');
32const buildGradlePath = path.join(ANDROID_DIR, 'build.gradle');
33const sdkVersionsPath = path.join(ANDROID_DIR, 'sdkVersions.json');
34const rnActivityPath = path.join(
35  expoviewPath,
36  'src/main/java/host/exp/exponent/experience/MultipleVersionReactNativeActivity.java'
37);
38const expoviewConstantsPath = path.join(
39  expoviewPath,
40  'src/main/java/host/exp/exponent/Constants.java'
41);
42const testSuiteTestsPath = path.join(
43  appPath,
44  'src/androidTest/java/host/exp/exponent/TestSuiteTests.java'
45);
46const reactAndroidPath = path.join(ANDROID_DIR, 'ReactAndroid');
47const reactCommonPath = path.join(ANDROID_DIR, 'ReactCommon');
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  }
228
229  return lib;
230}
231
232function processLine(line: string, abiVersion: string) {
233  if (
234    line.startsWith('LOCAL_MODULE') ||
235    line.startsWith('LOCAL_SHARED_LIBRARIES') ||
236    line.startsWith('LOCAL_STATIC_LIBRARIES')
237  ) {
238    let splitLine = line.split('=');
239    let libs = splitLine[1].split(' ');
240    for (let i = 0; i < libs.length; i++) {
241      libs[i] = renameLib(libs[i], abiVersion);
242    }
243    splitLine[1] = libs.join(' ');
244    line = splitLine.join('=');
245  }
246
247  return line;
248}
249
250async function processMkFileAsync(filename: string, abiVersion: string) {
251  let file = await fs.readFile(filename);
252  let fileString = file.toString();
253  await fs.truncate(filename, 0);
254  let lines = fileString.split('\n');
255  for (let i = 0; i < lines.length; i++) {
256    let line = lines[i];
257    line = processLine(line, abiVersion);
258    await fs.appendFile(filename, `${line}\n`);
259  }
260}
261
262async function processCMake(filePath: string, abiVersion: string) {
263  const libNameToReplace = new Set<string>();
264  for (const libName of JniLibNames) {
265    if (libName.startsWith('lib')) {
266      // in CMake we don't use the lib prefix
267      libNameToReplace.add(libName.slice(3));
268    } else {
269      libNameToReplace.add(libName);
270    }
271  }
272
273  libNameToReplace.delete('fb');
274  libNameToReplace.delete('fbjni'); // we use the prebuilt binary which is part of the `com.facebook.fbjni:fbjni`
275
276  for (const libName of libNameToReplace) {
277    await spawnAsync(
278      `sed -ri '' 's/${libName}([^\/]*$)/${libName}_abi${abiVersion}\\1/g' ${filePath}`,
279      [],
280      {
281        shell: true,
282      }
283    );
284  }
285}
286
287async function processJavaCodeAsync(libName: string, abiVersion: string) {
288  const abiName = `abi${abiVersion}`;
289  return spawnAsync(
290    `find ${versionedReactAndroidJavaPath} ${versionedExpoviewAbiPath(
291      abiName
292    )} -iname '*.java' -type f -print0 | ` +
293      `xargs -0 sed -i '' 's/"${libName}"/"${libName}_abi${abiVersion}"/g'`,
294    [],
295    { shell: true }
296  );
297}
298
299async function updateVersionedReactNativeAsync() {
300  await fs.remove(versionedReactAndroidPath);
301  await fs.remove(versionedReactCommonPath);
302  await fs.copy(reactAndroidPath, versionedReactAndroidPath);
303  await fs.copy(reactCommonPath, versionedReactCommonPath);
304}
305
306async function renameJniLibsAsync(version: string) {
307  const abiVersion = version.replace(/\./g, '_');
308  const abiPrefix = `abi${abiVersion}`;
309  const versionedAbiPath = path.join(
310    Directories.getAndroidDir(),
311    'versioned-abis',
312    `expoview-${abiPrefix}`
313  );
314
315  // Update JNI methods
316  const packagesToRename = await getJavaPackagesToRename();
317  for (const javaPackage of packagesToRename) {
318    const pathForPackage = javaPackage.replace(/\./g, '\\/');
319    await spawnAsync(
320      `find ${versionedReactCommonPath} ${versionedReactAndroidJniPath} -type f ` +
321        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
322        `xargs -0 sed -i '' 's/${pathForPackage}/abi${abiVersion}\\/${pathForPackage}/g'`,
323      [],
324      { shell: true }
325    );
326
327    // reanimated
328    const oldJNIReanimatedPackage =
329      'versioned\\/host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
330    const newJNIReanimatedPackage = 'host\\/exp\\/exponent\\/modules\\/api\\/reanimated\\/';
331    await spawnAsync(
332      `find ${versionedAbiPath} -type f ` +
333        `\\( -name \*.java -o -name \*.h -o -name \*.cpp -o -name \*.mk \\) -print0 | ` +
334        `xargs -0 sed -i '' 's/${oldJNIReanimatedPackage}/abi${abiVersion}\\/${newJNIReanimatedPackage}/g'`,
335      [],
336      { shell: true }
337    );
338  }
339
340  // Update LOCAL_MODULE, LOCAL_SHARED_LIBRARIES, LOCAL_STATIC_LIBRARIES fields in .mk files
341  let [reactCommonMkFiles, reactAndroidMkFiles, versionedAbiMKFiles] = await Promise.all([
342    glob(path.join(versionedReactCommonPath, '**/*.mk')),
343    glob(path.join(versionedReactAndroidJniPath, '**/*.mk')),
344    glob(path.join(versionedAbiPath, '**/*.mk')),
345  ]);
346  let filenames = [...reactCommonMkFiles, ...reactAndroidMkFiles, ...versionedAbiMKFiles];
347  await Promise.all(filenames.map((filename) => processMkFileAsync(filename, abiVersion)));
348
349  // Rename references to JNI libs in CMake
350  const cmakesFiles = await glob(path.join(versionedAbiPath, '**/CMakeLists.txt'));
351  await Promise.all(cmakesFiles.map((file) => processCMake(file, abiVersion)));
352
353  // Rename references to JNI libs in Java code
354  for (let i = 0; i < JniLibNames.length; i++) {
355    let libName = JniLibNames[i];
356    await processJavaCodeAsync(libName, abiVersion);
357  }
358
359  // 'fbjni' is loaded without the 'lib' prefix in com.facebook.jni.Prerequisites
360  await processJavaCodeAsync('fbjni', abiVersion);
361  await processJavaCodeAsync('fb', abiVersion);
362
363  console.log('\nThese are the JNI lib names we modified:');
364  await spawnAsync(
365    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "_abi${abiVersion}"`,
366    [],
367    { shell: true, stdio: 'inherit' }
368  );
369
370  console.log('\nAnd here are all instances of loadLibrary:');
371  await spawnAsync(
372    `find ${versionedReactAndroidJavaPath} ${versionedAbiPath} -name "*.java" | xargs grep -i "loadLibrary"`,
373    [],
374    { shell: true, stdio: 'inherit' }
375  );
376
377  const { isCorrect } = await inquirer.prompt<{ isCorrect: boolean }>([
378    {
379      type: 'confirm',
380      name: 'isCorrect',
381      message: 'Does all that look correct?',
382      default: false,
383    },
384  ]);
385  if (!isCorrect) {
386    throw new Error('Fix JNI libs');
387  }
388}
389
390async function copyUnimodulesAsync(version: string) {
391  const packages = await getListOfPackagesAsync();
392  for (const pkg of packages) {
393    if (
394      pkg.isSupportedOnPlatform('android') &&
395      pkg.isIncludedInExpoClientOnPlatform('android') &&
396      pkg.isVersionableOnPlatform('android')
397    ) {
398      await spawnAsync(
399        './android-copy-unimodule.sh',
400        [version, path.join(pkg.path, pkg.androidSubdirectory)],
401        {
402          shell: true,
403          cwd: SCRIPT_DIR,
404        }
405      );
406      console.log(`   ✅  Created versioned ${pkg.packageName}`);
407    }
408  }
409}
410
411async function addVersionedActivitesToManifests(version: string) {
412  const abiVersion = version.replace(/\./g, '_');
413  const abiName = `abi${abiVersion}`;
414  const majorVersion = semver.major(version);
415
416  await transformFileAsync(
417    templateManifestPath,
418    new RegExp('<!-- ADD DEV SETTINGS HERE -->'),
419    `<!-- ADD DEV SETTINGS HERE -->
420    <!-- BEGIN_SDK_${majorVersion} -->
421    <activity android:name="${abiName}.com.facebook.react.devsupport.DevSettingsActivity"/>
422    <!-- END_SDK_${majorVersion} -->`
423  );
424
425  await transformFileAsync(
426    templateManifestPath,
427    new RegExp('<!-- Versioned Activity for Stripe -->'),
428    `<!-- Versioned Activity for Stripe -->
429    <!-- BEGIN_SDK_${majorVersion} -->
430    <activity
431      android:exported="true"
432      android:launchMode="singleTask"
433      android:name="${abiName}.expo.modules.payments.stripe.RedirectUriReceiver"
434      android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen">
435      <intent-filter>
436        <action android:name="android.intent.action.VIEW" />
437        <category android:name="android.intent.category.DEFAULT" />
438        <category android:name="android.intent.category.BROWSABLE" />
439        <data android:scheme="${abiName}.expo.modules.payments.stripe" />
440      </intent-filter>
441    </activity>
442    <!-- END_SDK_${majorVersion} -->`
443  );
444}
445
446async function registerNewVersionUnderSdkVersions(version: string) {
447  let fileString = await fs.readFile(sdkVersionsPath, 'utf8');
448  let jsConfig;
449  // read the existing json config and add the new version to the sdkVersions array
450  try {
451    jsConfig = JSON.parse(fileString);
452  } catch (e) {
453    console.log('Error parsing existing sdkVersions.json file, writing a new one...', e);
454    console.log('The erroneous file contents was:', fileString);
455    jsConfig = {
456      sdkVersions: [],
457    };
458  }
459  // apply changes
460  jsConfig.sdkVersions.push(version);
461  await fs.writeFile(sdkVersionsPath, JSON.stringify(jsConfig));
462}
463
464async function cleanUpAsync(version: string) {
465  const abiVersion = version.replace(/\./g, '_');
466  const abiName = `abi${abiVersion}`;
467
468  const versionedAbiSrcPath = path.join(
469    versionedExpoviewAbiPath(abiName),
470    'src/main/java',
471    abiName
472  );
473
474  let filesToDelete: string[] = [];
475
476  // delete PrintDocumentAdapter*Callback.java
477  // their package is `android.print` and therefore they are not changed by the versioning script
478  // so we will have duplicate classes
479  const printCallbackFiles = await glob(
480    path.join(versionedAbiSrcPath, 'expo/modules/print/*Callback.java')
481  );
482  for (const file of printCallbackFiles) {
483    const contents = await fs.readFile(file, 'utf8');
484    if (!contents.includes(`package ${abiName}`)) {
485      filesToDelete.push(file);
486    } else {
487      console.log(`Skipping deleting ${file} because it appears to have been versioned`);
488    }
489  }
490
491  // delete versioned loader providers since we don't need them
492  filesToDelete.push(path.join(versionedAbiSrcPath, 'expo/loaders'));
493
494  console.log('Deleting the following files and directories:');
495  console.log(filesToDelete);
496
497  for (const file of filesToDelete) {
498    await fs.remove(file);
499  }
500
501  // misc fixes for versioned code
502  const versionedExponentPackagePath = path.join(
503    versionedAbiSrcPath,
504    'host/exp/exponent/ExponentPackage.java'
505  );
506  await transformFileAsync(
507    versionedExponentPackagePath,
508    new RegExp('// WHEN_VERSIONING_REMOVE_FROM_HERE', 'g'),
509    '/* WHEN_VERSIONING_REMOVE_FROM_HERE'
510  );
511  await transformFileAsync(
512    versionedExponentPackagePath,
513    new RegExp('// WHEN_VERSIONING_REMOVE_TO_HERE', 'g'),
514    'WHEN_VERSIONING_REMOVE_TO_HERE */'
515  );
516
517  await transformFileAsync(
518    path.join(versionedAbiSrcPath, 'host/exp/exponent/VersionedUtils.java'),
519    new RegExp('// DO NOT EDIT THIS COMMENT - used by versioning scripts[^,]+,[^,]+,'),
520    'null, null,'
521  );
522
523  await transformFileAsync(
524    path.join(versionedAbiSrcPath, 'expo/modules/payments/stripe/PayFlow.java'),
525    new RegExp('// ADD BUILDCONFIG IMPORT HERE'),
526    `import ${abiName}.host.exp.expoview.BuildConfig;`
527  );
528
529  // replace abixx_x_x...R with abixx_x_x.host.exp.expoview.R
530  await spawnAsync(
531    `find ${versionedAbiSrcPath} -iname '*.java' -type f -print0 | ` +
532      `xargs -0 sed -i '' 's/import ${abiName}\.[^;]*\.R;/import ${abiName}.host.exp.expoview.R;/g'`,
533    [],
534    { shell: true }
535  );
536  await spawnAsync(
537    `find ${versionedAbiSrcPath} -iname '*.kt' -type f -print0 | ` +
538      `xargs -0 sed -i '' 's/import ${abiName}\\..*\\.R$/import ${abiName}.host.exp.expoview.R/g'`,
539    [],
540    { shell: true }
541  );
542
543  // add new versioned maven to build.gradle
544  await transformFileAsync(
545    buildGradlePath,
546    new RegExp('// For old expoviews to work'),
547    `// For old expoviews to work
548    maven {
549      url "$rootDir/versioned-abis/expoview-${abiName}/maven"
550    }`
551  );
552}
553
554async function prepareReanimatedAsync(version: string): Promise<void> {
555  const abiVersion = version.replace(/\./g, '_');
556  const abiName = `abi${abiVersion}`;
557  const versionedExpoviewPath = versionedExpoviewAbiPath(abiName);
558
559  const buildReanimatedSO = async () => {
560    await spawnAsync(`./gradlew :expoview-${abiName}:packageNdkLibs`, [], {
561      shell: true,
562      cwd: path.join(versionedExpoviewPath, '../../'),
563      stdio: 'inherit',
564    });
565  };
566
567  const removeLeftoverDirectories = async () => {
568    const mainPath = path.join(versionedExpoviewPath, 'src', 'main');
569    const toRemove = ['Common', 'JNI', 'cpp'];
570    for (let dir of toRemove) {
571      await fs.remove(path.join(mainPath, dir));
572    }
573  };
574
575  const removeLeftoversFromGradle = async () => {
576    await spawnAsync('./android-remove-reanimated-code-from-gradle.sh', [version], {
577      shell: true,
578      cwd: SCRIPT_DIR,
579      stdio: 'inherit',
580    });
581  };
582
583  await buildReanimatedSO();
584  await removeLeftoverDirectories();
585  await removeLeftoversFromGradle();
586}
587
588async function exportReactNdks() {
589  const versionedRN = path.join(versionedReactAndroidPath, '..');
590  await spawnAsync(`./gradlew :ReactAndroid:packageReactNdkLibs`, [], {
591    shell: true,
592    cwd: versionedRN,
593    stdio: 'inherit',
594  });
595}
596
597async function exportReactNdksIfNeeded() {
598  const ndksPath = path.join(versionedReactAndroidPath, 'build', 'react-ndk', 'exported');
599  const exists = await fs.pathExists(ndksPath);
600  if (!exists) {
601    await exportReactNdks();
602    return;
603  }
604
605  const exportedSO = await glob(path.join(ndksPath, '**/*.so'));
606  if (exportedSO.length === 0) {
607    await exportReactNdks();
608  }
609}
610
611export async function addVersionAsync(version: string) {
612  console.log(' ��   1/10: Updating android/versioned-react-native...');
613  await updateVersionedReactNativeAsync();
614  console.log(' ✅  1/10: Finished\n\n');
615
616  console.log(' ��   2/10: Creating versioned expoview package...');
617  await spawnAsync('./android-copy-expoview.sh', [version], {
618    shell: true,
619    cwd: SCRIPT_DIR,
620  });
621
622  console.log(' ✅  2/10: Finished\n\n');
623
624  console.log(' ��   3/10: Renaming JNI libs in android/versioned-react-native and Reanimated...');
625  await renameJniLibsAsync(version);
626  console.log(' ✅  3/10: Finished\n\n');
627
628  console.log(' ��   4/10: Building versioned ReactAndroid AAR...');
629  await spawnAsync('./android-build-aar.sh', [version], {
630    shell: true,
631    cwd: SCRIPT_DIR,
632    stdio: 'inherit',
633  });
634  console.log(' ✅  4/10: Finished\n\n');
635
636  console.log(' ��   5/10: Exporting react ndks if needed...');
637  await exportReactNdksIfNeeded();
638  console.log(' ✅  5/10: Finished\n\n');
639
640  console.log(' ��   6/10: prepare versioned Reanimated...');
641  await prepareReanimatedAsync(version);
642  console.log(' ✅  6/10: Finished\n\n');
643
644  console.log(' ��   7/10: Creating versioned unimodule packages...');
645  await copyUnimodulesAsync(version);
646  console.log(' ✅  7/10: Finished\n\n');
647
648  console.log(' ��   8/10: Adding extra versioned activites to AndroidManifest...');
649  await addVersionedActivitesToManifests(version);
650  console.log(' ✅  8/10: Finished\n\n');
651
652  console.log(' ��   9/10: Registering new version under sdkVersions config...');
653  await registerNewVersionUnderSdkVersions(version);
654  console.log(' ✅  9/10: Finished\n\n');
655
656  console.log(' ��   10/10: Misc cleanup...');
657  await cleanUpAsync(version);
658  console.log(' ✅  10/10: Finished');
659}
660