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}:copyReleaseJniLibsProjectAndLocalJars`], 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 'stripped_native_libs', 92 'release', 93 'out', 94 'lib' 95 ); 96 const libFiles = await glob('**/*.so', { 97 cwd: buildLibDir, 98 }); 99 assert(libFiles.length > 0); 100 await Promise.all( 101 libFiles.map(async (file) => { 102 const srcPath = path.join(buildLibDir, file); 103 const archName = path.basename(path.dirname(file)); 104 const dstPath = path.join(jniLibDir, archName, path.basename(file)); 105 await fs.ensureDir(path.dirname(dstPath)); 106 await fs.copy(srcPath, dstPath); 107 }) 108 ); 109 110 // Truncate CMakeLists.txt and not to build this cxx module when building versioned Expo Go 111 await fs.writeFile(cmakeFile, ''); 112 await fs.remove(path.join(moduleRootDir, 'build')); 113} 114 115/** 116 * Transform ExponentPackage.kt, e.g. add import abi prefix 117 */ 118async function transformExponentPackageAsync(name: string, prefix: string) { 119 const transforms = exponentPackageTransforms(prefix)[name] ?? null; 120 const exponentPackageFile = path.resolve( 121 path.join( 122 ANDROID_DIR, 123 `versioned-abis/expoview-${prefix}/src/main/java/${prefix}/host/exp/exponent/ExponentPackage.kt` 124 ) 125 ); 126 await transformFileAsync(exponentPackageFile, transforms); 127} 128 129/** 130 * Gets the library name of each vendored module in a specific directory. 131 */ 132async function getVendoredModuleNamesAsync(directory: string): Promise<string[]> { 133 const vendoredGradlePaths = await glob(`**/build.gradle`, { 134 cwd: directory, 135 nodir: true, 136 realpath: true, 137 }); 138 139 const gradlePattern = new RegExp(`${directory}/(.*)/android/build.gradle`, 'i'); 140 141 return vendoredGradlePaths.reduce((result, gradlePath) => { 142 const moduleName = gradlePath.match(gradlePattern); 143 if (moduleName) { 144 result.push(moduleName[1]); 145 } 146 return result; 147 }, [] as string[]); 148} 149 150/** 151 * Removes the directory with vendored modules for given SDK number. 152 */ 153export async function removeVersionedVendoredModulesAsync(sdkNumber: number): Promise<void> { 154 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 155 await fs.remove(versionedDir); 156} 157 158/** 159 * Get the gradle dependency version from `android/expoview/build.gradle` 160 */ 161async function getGradleDependencyVersionFromExpoViewAsync( 162 group: string, 163 name: string 164): Promise<string | null> { 165 const expoviewGradleFile = path.join(ANDROID_DIR, 'expoview', 'build.gradle'); 166 const content = await fs.readFile(expoviewGradleFile, 'utf-8'); 167 const searchPattern = new RegExp( 168 `\\b(api|implementation)[\\s(]['"]${group}:${name}:(.+?)['"]`, 169 'g' 170 ); 171 const result = searchPattern.exec(content); 172 if (!result) { 173 return null; 174 } 175 return result[2]; 176} 177 178/** 179 * Generates base transforms to apply for all vendored modules. 180 */ 181async function baseTransformsFactoryAsync(prefix: string): Promise<Required<FileTransforms>> { 182 const fbjniVersion = await getGradleDependencyVersionFromExpoViewAsync( 183 'com.facebook.fbjni', 184 'fbjni-java-only' 185 ); 186 const proguardAnnotationVersion = await getGradleDependencyVersionFromExpoViewAsync( 187 'com.facebook.yoga', 188 'proguard-annotations' 189 ); 190 191 return { 192 path: [ 193 { 194 // For package renaming, src/main/java/* -> src/main/java/abiN/* 195 find: /\/(java|kotlin)\//, 196 replaceWith: `/$1/${prefix}/`, 197 }, 198 ], 199 content: [ 200 { 201 paths: '*.{java,kt}', 202 find: /(^package\s+)([\w.]+;?)/m, 203 replaceWith: `$1${prefix}.$2`, 204 }, 205 { 206 paths: '*.{java,kt}', 207 find: /(\bcom\.facebook\.(catalyst|csslayout|fbreact|hermes|perftest|quicklog|react|systrace|yoga|debug)\b)/g, 208 replaceWith: `${prefix}.$1`, 209 }, 210 { 211 paths: '*.{java,kt}', 212 find: /\b((System|SoLoader)\.loadLibrary\("[^"]*)("\);?)/g, 213 replaceWith: `$1_${prefix}$3`, 214 }, 215 { 216 paths: '*.{h,cpp}', 217 find: /(\bkJavaDescriptor\s*=\s*\n?\s*"L)/gm, 218 replaceWith: `$1${prefix}/`, 219 }, 220 { 221 paths: 'build.gradle', 222 find: /\b(compileOnly|implementation)\s+['"]com.facebook.react:react-native:.+['"]/gm, 223 replaceWith: 224 `implementation 'host.exp:reactandroid-${prefix}:1.0.0'` + 225 '\n' + 226 // Adding some compile time common dependencies where the versioned react-native AAR doesn't expose 227 ` compileOnly 'com.facebook.fbjni:fbjni:${fbjniVersion}'\n` + 228 ` compileOnly 'com.facebook.yoga:proguard-annotations:${proguardAnnotationVersion}'\n` + 229 ` compileOnly 'androidx.annotation:annotation:+'\n`, 230 }, 231 { 232 paths: 'build.gradle', 233 find: 'buildDir/react-native-0*/jni', 234 replaceWith: 'buildDir/reactandroid-abi*/jni', 235 }, 236 { 237 paths: ['build.gradle', 'CMakeLists.txt'], 238 find: /\/react-native\//g, 239 replaceWith: '/versioned-react-native/', 240 }, 241 { 242 paths: 'build.gradle', 243 find: /def rnAAR = fileTree.*\*\.aar.*\)/g, 244 replaceWith: `def rnAAR = fileTree("\${rootDir}/versioned-abis").matching({ include "**/reactandroid-${prefix}/**/*.aar" })`, 245 }, 246 { 247 paths: 'build.gradle', 248 find: /def rnAAR = fileTree.*rnAarMatcher.*\)/g, 249 replaceWith: `def rnAAR = fileTree("\${rootDir}/versioned-abis").matching({ include "**/reactandroid-${prefix}/**/*.aar" })`, 250 }, 251 { 252 paths: 'CMakeLists.txt', 253 find: /(^set\s*\(PACKAGE_NAME\s*['"])(\w+)(['"]\))/gm, 254 replaceWith: `$1$2_${prefix}$3`, 255 }, 256 { 257 paths: 'CMakeLists.txt', 258 find: /(\bfind_library\(\n?\s*[A-Z_]+\n?\s*)(\w+)/gm, 259 replaceWith(substring, group1, libName) { 260 if (['fbjni', 'log'].includes(libName)) { 261 return substring; 262 } 263 return `${group1}${libName}_${prefix}`; 264 }, 265 }, 266 { 267 paths: 'AndroidManifest.xml', 268 find: /(\bpackage=")([\w.]+)(")/, 269 replaceWith: `$1${prefix}.$2$3`, 270 }, 271 ], 272 }; 273} 274 275/** 276 * Returns the vendored directory for given SDK number. 277 */ 278function vendoredDirectoryForSDK(sdkNumber: number): string { 279 return path.join(ANDROID_VENDORED_DIR, `sdk${sdkNumber}`); 280} 281