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