1import spawnAsync from '@expo/spawn-async'; 2import chalk from 'chalk'; 3import fs from 'fs-extra'; 4import glob from 'glob-promise'; 5import minimatch from 'minimatch'; 6import path from 'path'; 7import semver from 'semver'; 8 9import * as Directories from '../../Directories'; 10import { getListOfPackagesAsync } from '../../Packages'; 11import { copyFileWithTransformsAsync } from '../../Transforms'; 12import { searchFilesAsync } from '../../Utils'; 13import { copyExpoviewAsync } from './copyExpoview'; 14import { expoModulesTransforms } from './expoModulesTransforms'; 15import { packagesToKeep } from './packagesConfig'; 16import { deleteLinesBetweenTags } from './utils'; 17import { versionCxxExpoModulesAsync } from './versionCxx'; 18import { updateVersionedReactNativeAsync } from './versionReactNative'; 19import { removeVersionedVendoredModulesAsync } from './versionVendoredModules'; 20 21export { versionVendoredModulesAsync } from './versionVendoredModules'; 22 23const EXPO_DIR = Directories.getExpoRepositoryRootDir(); 24const ANDROID_DIR = Directories.getAndroidDir(); 25const EXPOTOOLS_DIR = Directories.getExpotoolsDir(); 26const SCRIPT_DIR = path.join(EXPOTOOLS_DIR, 'src/versioning/android'); 27const SED_PREFIX = process.platform === 'darwin' ? "sed -i ''" : 'sed -i'; 28 29const appPath = path.join(ANDROID_DIR, 'app'); 30const expoviewPath = path.join(ANDROID_DIR, 'expoview'); 31const versionedAbisPath = path.join(ANDROID_DIR, 'versioned-abis'); 32const versionedExpoviewAbiPath = (abiName) => path.join(versionedAbisPath, `expoview-${abiName}`); 33const expoviewBuildGradlePath = path.join(expoviewPath, 'build.gradle'); 34const appManifestPath = path.join(appPath, 'src', 'main', 'AndroidManifest.xml'); 35const templateManifestPath = path.join( 36 EXPO_DIR, 37 'template-files', 38 'android', 39 'AndroidManifest.xml' 40); 41const settingsGradlePath = path.join(ANDROID_DIR, 'settings.gradle'); 42const appBuildGradlePath = path.join(appPath, 'build.gradle'); 43const buildGradlePath = path.join(ANDROID_DIR, 'build.gradle'); 44const sdkVersionsPath = path.join(ANDROID_DIR, 'sdkVersions.json'); 45const rnActivityPath = path.join( 46 expoviewPath, 47 'src/versioned/java/host/exp/exponent/experience/MultipleVersionReactNativeActivity.java' 48); 49const expoviewConstantsPath = path.join( 50 expoviewPath, 51 'src/main/java/host/exp/exponent/Constants.java' 52); 53const testSuiteTestsPath = path.join( 54 appPath, 55 'src/androidTest/java/host/exp/exponent/TestSuiteTests.kt' 56); 57const versionedReactAndroidPath = path.join(ANDROID_DIR, 'versioned-react-native/ReactAndroid'); 58const versionedHermesPath = path.join(ANDROID_DIR, 'versioned-react-native/sdks/hermes'); 59 60async function transformFileAsync(filePath: string, regexp: RegExp, replacement: string = '') { 61 const fileContent = await fs.readFile(filePath, 'utf8'); 62 await fs.writeFile(filePath, fileContent.replace(regexp, replacement)); 63} 64 65async function removeVersionReferencesFromFileAsync(sdkMajorVersion: string, filePath: string) { 66 console.log( 67 `Removing code surrounded by ${chalk.gray(`// BEGIN_SDK_${sdkMajorVersion}`)} and ${chalk.gray( 68 `// END_SDK_${sdkMajorVersion}` 69 )} from ${chalk.magenta(path.relative(EXPO_DIR, filePath))}...` 70 ); 71 await transformFileAsync( 72 filePath, 73 new RegExp( 74 `\\s*//\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\n.*?//\\s*END_SDK_${sdkMajorVersion}(_\d+)*`, 75 'gs' 76 ), 77 '' 78 ); 79} 80 81async function removeVersionedExpoviewAsync(versionedExpoviewAbiPath: string) { 82 console.log( 83 `Removing versioned expoview at ${chalk.magenta( 84 path.relative(EXPO_DIR, versionedExpoviewAbiPath) 85 )}...` 86 ); 87 await fs.remove(versionedExpoviewAbiPath); 88} 89 90async function removeFromManifestAsync(sdkMajorVersion: string, manifestPath: string) { 91 console.log( 92 `Removing code surrounded by ${chalk.gray( 93 `<!-- BEGIN_SDK_${sdkMajorVersion} -->` 94 )} and ${chalk.gray(`<!-- END_SDK_${sdkMajorVersion} -->`)} from ${chalk.magenta( 95 path.relative(EXPO_DIR, manifestPath) 96 )}...` 97 ); 98 await transformFileAsync( 99 manifestPath, 100 new RegExp( 101 `\\s*<!--\\s*BEGIN_SDK_${sdkMajorVersion}(_\d+)*\\s*-->.*?<!--\\s*END_SDK_${sdkMajorVersion}(_\d+)*\\s*-->`, 102 'gs' 103 ), 104 '' 105 ); 106} 107 108async function removeFromSettingsGradleAsync(abiName: string, settingsGradlePath: string) { 109 console.log( 110 `Removing ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta( 111 path.relative(EXPO_DIR, settingsGradlePath) 112 )}...` 113 ); 114 const sdkVersion = abiName.replace(/abi(\d+)_0_0/, 'sdk$1'); 115 await transformFileAsync(settingsGradlePath, new RegExp(`\\n\\s*"${abiName}",[^\\n]*`, 'g'), ''); 116 await transformFileAsync( 117 settingsGradlePath, 118 new RegExp(`\\nuseVendoredModulesForSettingsGradle\\('${sdkVersion}'\\)[^\\n]*`, 'g'), 119 '' 120 ); 121} 122 123async function removeFromBuildGradleAsync(abiName: string, buildGradlePath: string) { 124 console.log( 125 `Removing maven repository for ${chalk.green(`expoview-${abiName}`)} from ${chalk.magenta( 126 path.relative(EXPO_DIR, buildGradlePath) 127 )}...` 128 ); 129 await transformFileAsync( 130 buildGradlePath, 131 new RegExp(`\\s*maven\\s*{\\s*url\\s*".*?/expoview-${abiName}/maven"\\s*}[^\\n]*`), 132 '' 133 ); 134} 135 136async function removeFromSdkVersionsAsync(version: string, sdkVersionsPath: string) { 137 console.log( 138 `Removing ${chalk.cyan(version)} from ${chalk.magenta( 139 path.relative(EXPO_DIR, sdkVersionsPath) 140 )}...` 141 ); 142 await transformFileAsync(sdkVersionsPath, new RegExp(`"${version}",\s*`, 'g'), ''); 143} 144 145async function removeTestSuiteTestsAsync(version: string, testsFilePath: string) { 146 console.log( 147 `Removing test-suite tests from ${chalk.magenta(path.relative(EXPO_DIR, testsFilePath))}...` 148 ); 149 await transformFileAsync( 150 testsFilePath, 151 new RegExp(`\\s*(@\\w+\\s+)*@ExpoSdkVersionTest\\("${version}"\\)[^}]+}`), 152 '' 153 ); 154} 155 156async function findAndPrintVersionReferencesInSourceFilesAsync(version: string): Promise<boolean> { 157 const pattern = new RegExp( 158 `(${version.replace(/\./g, '[._]')}|(SDK|ABI).?${semver.major(version)})`, 159 'ig' 160 ); 161 let matchesCount = 0; 162 163 const files = await glob('**/{src/**/*.@(java|kt|xml),build.gradle}', { cwd: ANDROID_DIR }); 164 165 for (const file of files) { 166 const filePath = path.join(ANDROID_DIR, file); 167 const fileContent = await fs.readFile(filePath, 'utf8'); 168 const fileLines = fileContent.split(/\r\n?|\n/g); 169 let match; 170 171 while ((match = pattern.exec(fileContent)) != null) { 172 const index = pattern.lastIndex - match[0].length; 173 const lineNumberWithMatch = fileContent.substring(0, index).split(/\r\n?|\n/g).length - 1; 174 const firstLineInContext = Math.max(0, lineNumberWithMatch - 2); 175 const lastLineInContext = Math.min(lineNumberWithMatch + 2, fileLines.length); 176 177 ++matchesCount; 178 179 console.log( 180 `Found ${chalk.bold.green(match[0])} in ${chalk.magenta( 181 path.relative(EXPO_DIR, filePath) 182 )}:` 183 ); 184 185 for (let lineIndex = firstLineInContext; lineIndex <= lastLineInContext; lineIndex++) { 186 console.log( 187 `${chalk.gray(1 + lineIndex + ':')} ${fileLines[lineIndex].replace( 188 match[0], 189 chalk.bgMagenta(match[0]) 190 )}` 191 ); 192 } 193 console.log(); 194 } 195 } 196 return matchesCount > 0; 197} 198 199export async function removeVersionAsync(version: string) { 200 const abiName = `abi${version.replace(/\./g, '_')}`; 201 const sdkMajorVersion = `${semver.major(version)}`; 202 203 console.log(`Removing SDK version ${chalk.cyan(version)} for ${chalk.blue('Android')}...`); 204 205 // Remove expoview-abi*_0_0 library 206 await removeVersionedExpoviewAsync(versionedExpoviewAbiPath(abiName)); 207 await removeFromSettingsGradleAsync(abiName, settingsGradlePath); 208 await removeFromBuildGradleAsync(abiName, buildGradlePath); 209 210 // Remove code surrounded by BEGIN_SDK_* and END_SDK_* 211 await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewBuildGradlePath); 212 await removeVersionReferencesFromFileAsync(sdkMajorVersion, appBuildGradlePath); 213 await removeVersionReferencesFromFileAsync(sdkMajorVersion, rnActivityPath); 214 await removeVersionReferencesFromFileAsync(sdkMajorVersion, expoviewConstantsPath); 215 216 // Remove test-suite tests from the app. 217 await removeTestSuiteTestsAsync(version, testSuiteTestsPath); 218 219 // Update AndroidManifests 220 await removeFromManifestAsync(sdkMajorVersion, appManifestPath); 221 await removeFromManifestAsync(sdkMajorVersion, templateManifestPath); 222 223 // Remove vendored modules 224 await removeVersionedVendoredModulesAsync(Number(version)); 225 226 // Remove SDK version from the list of supported SDKs 227 await removeFromSdkVersionsAsync(version, sdkVersionsPath); 228 229 console.log(`\nLooking for SDK references in source files...`); 230 231 if (await findAndPrintVersionReferencesInSourceFilesAsync(version)) { 232 console.log( 233 chalk.yellow(`Please review all of these references and remove them manually if possible!\n`) 234 ); 235 } 236} 237 238async function copyExpoModulesAsync(version: string) { 239 const packages = await getListOfPackagesAsync(); 240 for (const pkg of packages) { 241 if ( 242 pkg.isSupportedOnPlatform('android') && 243 pkg.isIncludedInExpoClientOnPlatform('android') && 244 pkg.isVersionableOnPlatform('android') 245 ) { 246 const abiVersion = `abi${version.replace(/\./g, '_')}`; 247 const targetDirectory = path.join(ANDROID_DIR, `versioned-abis/expoview-${abiVersion}`); 248 const sourceDirectory = path.join(pkg.path, pkg.androidSubdirectory); 249 const transforms = expoModulesTransforms(pkg.packageName, abiVersion); 250 251 const files = await searchFilesAsync(sourceDirectory, [ 252 './src/main/java/**', 253 './src/main/kotlin/**', 254 './src/main/AndroidManifest.xml', 255 ]); 256 257 for (const javaPkg of packagesToKeep) { 258 const javaPkgWithSlash = javaPkg.replace(/\./g, '/'); 259 const pathFromPackage = `./src/main/{java,kotlin}/${javaPkgWithSlash}{/**,.java,.kt}`; 260 for (const file of files) { 261 if (minimatch(file, pathFromPackage)) { 262 files.delete(file); 263 continue; 264 } 265 } 266 } 267 268 for (const sourceFile of files) { 269 await copyFileWithTransformsAsync({ 270 sourceFile, 271 targetDirectory, 272 sourceDirectory, 273 transforms, 274 }); 275 } 276 const temporaryPackageManifestPath = path.join( 277 targetDirectory, 278 'src/main/TemporaryExpoModuleAndroidManifest.xml' 279 ); 280 const mainManifestPath = path.join(targetDirectory, 'src/main/AndroidManifest.xml'); 281 await spawnAsync('java', [ 282 '-jar', 283 path.join(SCRIPT_DIR, 'android-manifest-merger-3898d3a.jar'), 284 '--main', 285 mainManifestPath, 286 '--libs', 287 temporaryPackageManifestPath, 288 '--placeholder', 289 'applicationId=${applicationId}', 290 '--out', 291 mainManifestPath, 292 '--log', 293 'WARNING', 294 ]); 295 await fs.remove(temporaryPackageManifestPath); 296 console.log(` ✅ Created versioned ${pkg.packageName}`); 297 } 298 } 299} 300 301async function addVersionedActivitesToManifests(version: string) { 302 const abiVersion = version.replace(/\./g, '_'); 303 const abiName = `abi${abiVersion}`; 304 const majorVersion = semver.major(version); 305 306 await transformFileAsync( 307 templateManifestPath, 308 new RegExp('<!-- ADD DEV SETTINGS HERE -->'), 309 `<!-- ADD DEV SETTINGS HERE --> 310 <!-- BEGIN_SDK_${majorVersion} --> 311 <activity android:name="${abiName}.com.facebook.react.devsupport.DevSettingsActivity"/> 312 <!-- END_SDK_${majorVersion} -->` 313 ); 314} 315 316async function registerNewVersionUnderSdkVersions(version: string) { 317 const fileString = await fs.readFile(sdkVersionsPath, 'utf8'); 318 let jsConfig; 319 // read the existing json config and add the new version to the sdkVersions array 320 try { 321 jsConfig = JSON.parse(fileString); 322 } catch (e) { 323 console.log('Error parsing existing sdkVersions.json file, writing a new one...', e); 324 console.log('The erroneous file contents was:', fileString); 325 jsConfig = { 326 sdkVersions: [], 327 }; 328 } 329 // apply changes 330 jsConfig.sdkVersions.push(version); 331 await fs.writeFile(sdkVersionsPath, JSON.stringify(jsConfig)); 332} 333 334async function cleanUpAsync(version: string) { 335 const abiVersion = version.replace(/\./g, '_'); 336 const abiName = `abi${abiVersion}`; 337 338 const versionedAbiSrcPath = path.join( 339 versionedExpoviewAbiPath(abiName), 340 'src/main/java', 341 abiName 342 ); 343 344 const filesToDelete: string[] = []; 345 346 // delete PrintDocumentAdapter*Callback.kt 347 // their package is `android.print` and therefore they are not changed by the versioning script 348 // so we will have duplicate classes 349 const printCallbackFiles = await glob( 350 path.join(versionedAbiSrcPath, 'expo/modules/print/*Callback.kt') 351 ); 352 for (const file of printCallbackFiles) { 353 const contents = await fs.readFile(file, 'utf8'); 354 if (!contents.includes(`package ${abiName}`)) { 355 filesToDelete.push(file); 356 } else { 357 console.log(`Skipping deleting ${file} because it appears to have been versioned`); 358 } 359 } 360 361 // delete versioned loader providers since we don't need them 362 filesToDelete.push(path.join(versionedAbiSrcPath, 'expo/loaders')); 363 364 console.log('Deleting the following files and directories:'); 365 console.log(filesToDelete); 366 367 for (const file of filesToDelete) { 368 await fs.remove(file); 369 } 370 371 // misc fixes for versioned code 372 const versionedExponentPackagePath = path.join( 373 versionedAbiSrcPath, 374 'host/exp/exponent/ExponentPackage.kt' 375 ); 376 await transformFileAsync( 377 versionedExponentPackagePath, 378 new RegExp('// WHEN_VERSIONING_REMOVE_FROM_HERE', 'g'), 379 '/* WHEN_VERSIONING_REMOVE_FROM_HERE' 380 ); 381 await transformFileAsync( 382 versionedExponentPackagePath, 383 new RegExp('// WHEN_VERSIONING_REMOVE_TO_HERE', 'g'), 384 'WHEN_VERSIONING_REMOVE_TO_HERE */' 385 ); 386 387 await transformFileAsync( 388 path.join(versionedAbiSrcPath, 'host/exp/exponent/VersionedUtils.kt'), 389 new RegExp('// DO NOT EDIT THIS COMMENT - used by versioning scripts[^,]+,[^,]+,'), 390 'null, null,' 391 ); 392 393 // replace abixx_x_x...R with abixx_x_x.host.exp.expoview.R 394 await spawnAsync( 395 `find ${versionedAbiSrcPath} -iname '*.java' -type f -print0 | ` + 396 `xargs -0 ${SED_PREFIX} 's/import ${abiName}\.[^;]*\.R;/import ${abiName}.host.exp.expoview.R;/g'`, 397 [], 398 { shell: true } 399 ); 400 await spawnAsync( 401 `find ${versionedAbiSrcPath} -iname '*.kt' -type f -print0 | ` + 402 `xargs -0 ${SED_PREFIX} 's/import ${abiName}\\..*\\.R$/import ${abiName}.host.exp.expoview.R/g'`, 403 [], 404 { shell: true } 405 ); 406 407 // add new versioned maven to build.gradle 408 await transformFileAsync( 409 buildGradlePath, 410 new RegExp('// For old expoviews to work'), 411 `// For old expoviews to work 412 maven { 413 url "$rootDir/versioned-abis/expoview-${abiName}/maven" 414 }` 415 ); 416} 417 418async function prepareReanimatedAsync(version: string): Promise<void> { 419 const abiVersion = version.replace(/\./g, '_'); 420 const abiName = `abi${abiVersion}`; 421 const versionedExpoviewPath = versionedExpoviewAbiPath(abiName); 422 const buildGradlePath = path.join(versionedExpoviewPath, 'build.gradle'); 423 424 const buildReanimatedSO = async () => { 425 await spawnAsync(`./gradlew :expoview-${abiName}:packageNdkLibs`, [], { 426 shell: true, 427 cwd: path.join(versionedExpoviewPath, '../../'), 428 stdio: 'inherit', 429 }); 430 }; 431 432 const removeLeftoverDirectories = async () => { 433 const mainPath = path.join(versionedExpoviewPath, 'src', 'main'); 434 const toRemove = ['Common', 'JNI', 'cpp']; 435 for (const dir of toRemove) { 436 await fs.remove(path.join(mainPath, dir)); 437 } 438 }; 439 440 const removeLeftoversFromGradle = async () => { 441 const buildGradle = await fs.readFile(buildGradlePath, 'utf-8'); 442 await fs.writeFile( 443 buildGradlePath, 444 deleteLinesBetweenTags( 445 /WHEN_PREPARING_REANIMATED_REMOVE_FROM_HERE/, 446 /WHEN_PREPARING_REANIMATED_REMOVE_TO_HERE/, 447 buildGradle 448 ) 449 ); 450 }; 451 452 await buildReanimatedSO(); 453 await removeLeftoverDirectories(); 454 await removeLeftoversFromGradle(); 455} 456 457async function exportReactNdks() { 458 const versionedRN = path.join(versionedReactAndroidPath, '..'); 459 await spawnAsync(`./gradlew :ReactAndroid:packageReactNdkLibs`, [], { 460 shell: true, 461 cwd: versionedRN, 462 stdio: 'inherit', 463 env: { 464 ...process.env, 465 REACT_NATIVE_OVERRIDE_HERMES_DIR: versionedHermesPath, 466 }, 467 }); 468} 469 470async function exportReactNdksIfNeeded() { 471 const ndksPath = path.join(versionedReactAndroidPath, 'build', 'react-ndk', 'exported'); 472 const exists = await fs.pathExists(ndksPath); 473 if (!exists) { 474 await exportReactNdks(); 475 return; 476 } 477 478 const exportedSO = await glob(path.join(ndksPath, '**/*.so')); 479 if (exportedSO.length === 0) { 480 await exportReactNdks(); 481 } 482} 483 484export async function addVersionAsync(version: string) { 485 console.log(' 1/10: Updating android/versioned-react-native...'); 486 await updateVersionedReactNativeAsync( 487 Directories.getReactNativeSubmoduleDir(), 488 ANDROID_DIR, 489 version 490 ); 491 console.log(' ✅ 1/10: Finished\n\n'); 492 493 console.log(' 2/10: Building versioned ReactAndroid AAR...'); 494 await spawnAsync('./android-build-aar.sh', [version], { 495 shell: true, 496 cwd: SCRIPT_DIR, 497 stdio: 'inherit', 498 }); 499 console.log(' ✅ 2/10: Finished\n\n'); 500 501 console.log(' 3/10: Creating versioned expoview package...'); 502 await copyExpoviewAsync(version, ANDROID_DIR); 503 console.log(' ✅ 3/10: Finished\n\n'); 504 505 console.log(' 4/10: Exporting react ndks if needed...'); 506 await exportReactNdksIfNeeded(); 507 console.log(' ✅ 4/10: Finished\n\n'); 508 509 console.log(' 5/10: prepare versioned Reanimated...'); 510 await prepareReanimatedAsync(version); 511 console.log(' ✅ 5/10: Finished\n\n'); 512 513 console.log(' 6/10: Creating versioned expo-modules packages...'); 514 await copyExpoModulesAsync(version); 515 console.log(' ✅ 6/10: Finished\n\n'); 516 517 console.log(' 7/10: Versoning c++ libraries for expo-modules...'); 518 await versionCxxExpoModulesAsync(version); 519 console.log(' ✅ 7/10: Finished\n\n'); 520 521 console.log(' 8/10: Adding extra versioned activites to AndroidManifest...'); 522 await addVersionedActivitesToManifests(version); 523 console.log(' ✅ 8/10: Finished\n\n'); 524 525 console.log(' 9/10: Registering new version under sdkVersions config...'); 526 await registerNewVersionUnderSdkVersions(version); 527 console.log(' ✅ 9/10: Finished\n\n'); 528 529 console.log(' 10/10: Misc cleanup...'); 530 await cleanUpAsync(version); 531 console.log(' ✅ 10/10: Finished'); 532} 533