1import spawnAsync from '@expo/spawn-async'; 2import assert from 'assert'; 3import chalk from 'chalk'; 4import fs from 'fs-extra'; 5import glob from 'glob-promise'; 6import path from 'path'; 7 8import { ANDROID_DIR } from '../../Constants'; 9import logger from '../../Logger'; 10import { copyFileWithTransformsAsync, transformFileAsync } from '../../Transforms'; 11import { FileTransforms } from '../../Transforms.types'; 12import { searchFilesAsync } from '../../Utils'; 13import { 14 exponentPackageTransforms, 15 vendoredModulesTransforms, 16} from './transforms/vendoredModulesTransforms'; 17 18const ANDROID_VENDORED_DIR = path.join(ANDROID_DIR, 'vendored'); 19 20/** 21 * Versions Android vendored modules. 22 */ 23export async function versionVendoredModulesAsync( 24 sdkNumber: number, 25 filterModules: string[] | null 26): Promise<void> { 27 const prefix = `abi${sdkNumber}_0_0`; 28 const config = vendoredModulesTransforms(prefix); 29 const baseTransforms = await baseTransformsFactoryAsync(prefix); 30 const unversionedDir = path.join(ANDROID_VENDORED_DIR, 'unversioned'); 31 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 32 let vendoredModuleNames = await getVendoredModuleNamesAsync(unversionedDir); 33 if (filterModules) { 34 vendoredModuleNames = vendoredModuleNames.filter((name) => filterModules.includes(name)); 35 } 36 37 for (const name of vendoredModuleNames) { 38 logger.info(' Versioning vendored module %s', chalk.green(name)); 39 40 const moduleConfig = config[name]; 41 const sourceDirectory = path.join(unversionedDir, name); 42 const targetDirectory = path.join(versionedDir, name); 43 const files = await searchFilesAsync(sourceDirectory, '**'); 44 45 await fs.remove(targetDirectory); 46 47 for (const sourceFile of files) { 48 await copyFileWithTransformsAsync({ 49 sourceFile, 50 sourceDirectory, 51 targetDirectory, 52 transforms: { 53 path: [...baseTransforms.path, ...(moduleConfig?.path ?? [])], 54 content: [...baseTransforms.content, ...(moduleConfig?.content ?? [])], 55 }, 56 }); 57 } 58 59 await maybePrebuildSharedLibsAsync(name, sdkNumber); 60 await transformExponentPackageAsync(name, prefix); 61 } 62} 63 64/** 65 * Prebuild shared libraries to jniLibs and cleanup CMakeLists.txt 66 */ 67async function maybePrebuildSharedLibsAsync(module: string, sdkNumber: number) { 68 const moduleRootDir = path.join(ANDROID_DIR, 'vendored', `sdk${sdkNumber}`, module, 'android'); 69 const cmakeFile = path.join(moduleRootDir, 'CMakeLists.txt'); 70 if (!fs.existsSync(cmakeFile)) { 71 return; 72 } 73 74 logger.info(' Prebuilding shared libraries for %s', module); 75 const gradleProject = module.replace(/\//g, '_'); 76 await spawnAsync( 77 './gradlew', 78 [`:vendored_sdk${sdkNumber}_${gradleProject}:copyReleaseJniLibsProjectOnly`], 79 { 80 cwd: ANDROID_DIR, 81 // Uncomment the following line for verbose building output 82 // stdio: 'inherit', 83 } 84 ); 85 86 const jniLibDir = path.join(moduleRootDir, 'src', 'main', 'jniLibs'); 87 const buildLibDir = path.join( 88 moduleRootDir, 89 'build', 90 'intermediates', 91 'library_jni', 92 'release', 93 'jni' 94 ); 95 const libFiles = await glob('**/*.so', { 96 cwd: buildLibDir, 97 }); 98 assert(libFiles.length >= 0); 99 await Promise.all( 100 libFiles.map(async (file) => { 101 const srcPath = path.join(buildLibDir, file); 102 const archName = path.basename(path.dirname(file)); 103 const dstPath = path.join(jniLibDir, archName, path.basename(file)); 104 await fs.ensureDir(path.dirname(dstPath)); 105 await fs.copy(srcPath, dstPath); 106 }) 107 ); 108 109 // Truncate CMakeLists.txt and not to build this cxx module when building versioned Expo Go 110 await fs.writeFile(cmakeFile, ''); 111 await fs.remove(path.join(moduleRootDir, 'build')); 112} 113 114/** 115 * Transform ExponentPackage.kt, e.g. add import abi prefix 116 */ 117async function transformExponentPackageAsync(name: string, prefix: string) { 118 const transforms = exponentPackageTransforms(prefix)[name] ?? null; 119 const exponentPackageFile = path.resolve( 120 path.join( 121 ANDROID_DIR, 122 `versioned-abis/expoview-${prefix}/src/main/java/${prefix}/host/exp/exponent/ExponentPackage.kt` 123 ) 124 ); 125 await transformFileAsync(exponentPackageFile, transforms); 126} 127 128/** 129 * Gets the library name of each vendored module in a specific directory. 130 */ 131async function getVendoredModuleNamesAsync(directory: string): Promise<string[]> { 132 const vendoredGradlePaths = await glob(`**/build.gradle`, { 133 cwd: directory, 134 nodir: true, 135 realpath: true, 136 }); 137 138 const gradlePattern = new RegExp(`${directory}/(.*)/android/build.gradle`, 'i'); 139 140 return vendoredGradlePaths.reduce((result, gradlePath) => { 141 const moduleName = gradlePath.match(gradlePattern); 142 if (moduleName) { 143 result.push(moduleName[1]); 144 } 145 return result; 146 }, [] as string[]); 147} 148 149/** 150 * Removes the directory with vendored modules for given SDK number. 151 */ 152export async function removeVersionedVendoredModulesAsync(sdkNumber: number): Promise<void> { 153 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 154 await fs.remove(versionedDir); 155} 156 157/** 158 * Get the gradle dependency version from `android/expoview/build.gradle` 159 */ 160async function getGradleDependencyVersionFromExpoViewAsync( 161 group: string, 162 name: string 163): Promise<string | null> { 164 const expoviewGradleFile = path.join(ANDROID_DIR, 'expoview', 'build.gradle'); 165 const content = await fs.readFile(expoviewGradleFile, 'utf-8'); 166 const searchPattern = new RegExp( 167 `\\b(api|implementation)[\\s(]['"]${group}:${name}:(.+?)['"]`, 168 'g' 169 ); 170 const result = searchPattern.exec(content); 171 if (!result) { 172 return null; 173 } 174 return result[2]; 175} 176 177/** 178 * Generates base transforms to apply for all vendored modules. 179 */ 180async function baseTransformsFactoryAsync(prefix: string): Promise<Required<FileTransforms>> { 181 const fbjniVersion = await getGradleDependencyVersionFromExpoViewAsync( 182 'com.facebook.fbjni', 183 'fbjni-java-only' 184 ); 185 const proguardAnnotationVersion = await getGradleDependencyVersionFromExpoViewAsync( 186 'com.facebook.yoga', 187 'proguard-annotations' 188 ); 189 190 return { 191 path: [ 192 { 193 // For package renaming, src/main/java/* -> src/main/java/abiN/* 194 find: /\/(java|kotlin)\//, 195 replaceWith: `/$1/${prefix}/`, 196 }, 197 ], 198 content: [ 199 { 200 paths: '*.{java,kt}', 201 find: /(^package\s+)([\w.]+;?)/m, 202 replaceWith: `$1${prefix}.$2`, 203 }, 204 { 205 paths: '*.{java,kt}', 206 find: /(\bcom\.facebook\.(catalyst|csslayout|fbreact|hermes|perftest|quicklog|react|systrace|yoga|debug)\b)/g, 207 replaceWith: `${prefix}.$1`, 208 }, 209 { 210 paths: '*.{java,kt}', 211 find: /\b((System|SoLoader)\.loadLibrary\("[^"]*)("\);?)/g, 212 replaceWith: `$1_${prefix}$3`, 213 }, 214 { 215 paths: '*.{h,cpp}', 216 find: /(\bkJavaDescriptor\s*=\s*\n?\s*"L)/gm, 217 replaceWith: `$1${prefix}/`, 218 }, 219 { 220 paths: 'build.gradle', 221 find: /\b(compileOnly|implementation)\s+['"]com.facebook.react:react-native:.+['"]/gm, 222 replaceWith: 223 `implementation 'host.exp:reactandroid-${prefix}:1.0.0'` + 224 '\n' + 225 // Adding some compile time common dependencies where the versioned react-native AAR doesn't expose 226 ` compileOnly 'com.facebook.fbjni:fbjni:${fbjniVersion}'\n` + 227 ` compileOnly 'com.facebook.yoga:proguard-annotations:${proguardAnnotationVersion}'\n` + 228 ` compileOnly 'androidx.annotation:annotation:+'\n`, 229 }, 230 { 231 paths: 'build.gradle', 232 find: 'buildDir/react-native-0*/jni', 233 replaceWith: 'buildDir/reactandroid-abi*/jni', 234 }, 235 { 236 paths: ['build.gradle', 'CMakeLists.txt'], 237 find: /\/react-native\//g, 238 replaceWith: '/versioned-react-native/', 239 }, 240 { 241 paths: 'build.gradle', 242 find: /def rnAAR = fileTree.*\*\.aar.*\)/g, 243 replaceWith: `def rnAAR = fileTree("\${rootDir}/versioned-abis").matching({ include "**/reactandroid-${prefix}/**/*.aar" })`, 244 }, 245 { 246 paths: 'build.gradle', 247 find: /def rnAAR = fileTree.*rnAarMatcher.*\)/g, 248 replaceWith: `def rnAAR = fileTree("\${rootDir}/versioned-abis").matching({ include "**/reactandroid-${prefix}/**/*.aar" })`, 249 }, 250 { 251 paths: 'CMakeLists.txt', 252 find: /(^set\s*\(PACKAGE_NAME\s*['"])(\w+)(['"]\))/gm, 253 replaceWith: `$1$2_${prefix}$3`, 254 }, 255 { 256 paths: 'CMakeLists.txt', 257 find: /(\bfind_library\(\n?\s*[A-Z_]+\n?\s*)(\w+)/gm, 258 replaceWith(substring, group1, libName) { 259 if (['fbjni', 'log'].includes(libName)) { 260 return substring; 261 } 262 return `${group1}${libName}_${prefix}`; 263 }, 264 }, 265 { 266 paths: 'AndroidManifest.xml', 267 find: /(\bpackage=")([\w.]+)(")/, 268 replaceWith: `$1${prefix}.$2$3`, 269 }, 270 ], 271 }; 272} 273 274/** 275 * Returns the vendored directory for given SDK number. 276 */ 277function vendoredDirectoryForSDK(sdkNumber: number): string { 278 return path.join(ANDROID_VENDORED_DIR, `sdk${sdkNumber}`); 279} 280