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