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