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