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