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