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