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, transformFilesAsync } 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 basenames = [ 121 'ExponentPackage', 122 'ExponentAsyncStorageModule', 123 'ExponentUnsignedAsyncStorageModule', 124 ]; 125 const files = await glob(`**/{${basenames.join(',')}}.kt`, { 126 cwd: path.join(ANDROID_DIR, `versioned-abis/expoview-${prefix}`), 127 nodir: true, 128 absolute: true, 129 }); 130 await transformFilesAsync(files, transforms); 131} 132 133/** 134 * Gets the library name of each vendored module in a specific directory. 135 */ 136async function getVendoredModuleNamesAsync(directory: string): Promise<string[]> { 137 const vendoredGradlePaths = await glob(`**/build.gradle`, { 138 cwd: directory, 139 nodir: true, 140 realpath: true, 141 }); 142 143 const gradlePattern = new RegExp(`${directory}/(.*)/android/build.gradle`, 'i'); 144 145 return vendoredGradlePaths.reduce((result, gradlePath) => { 146 const moduleName = gradlePath.match(gradlePattern); 147 if (moduleName) { 148 result.push(moduleName[1]); 149 } 150 return result; 151 }, [] as string[]); 152} 153 154/** 155 * Removes the directory with vendored modules for given SDK number. 156 */ 157export async function removeVersionedVendoredModulesAsync(version: string): Promise<void> { 158 const sdkNumber = Number(version.split('.')[0]); 159 const versionedDir = vendoredDirectoryForSDK(sdkNumber); 160 await fs.remove(versionedDir); 161} 162 163/** 164 * Generates base transforms to apply for all vendored modules. 165 */ 166async function baseTransformsFactoryAsync(prefix: string): Promise<Required<FileTransforms>> { 167 return { 168 path: [ 169 { 170 // For package renaming, src/main/java/* -> src/main/java/abiN/* 171 find: /\/(java|kotlin)\//, 172 replaceWith: `/$1/${prefix}/`, 173 }, 174 ], 175 content: [ 176 { 177 paths: '*.{java,kt}', 178 find: /(^package\s+)([\w.]+;?)/m, 179 replaceWith: `$1${prefix}.$2`, 180 }, 181 { 182 paths: '*.{java,kt}', 183 find: new RegExp( 184 `\\b(?<!${prefix}\\.)(com\\.facebook\\.(catalyst|csslayout|fbreact|hermes|perftest|quicklog|react|systrace|yoga|debug)\\b)`, 185 'g' 186 ), 187 replaceWith: `${prefix}.$1`, 188 }, 189 { 190 paths: '*.{java,kt}', 191 find: /\bimport (com\.swmansion\.)/g, 192 replaceWith: `import ${prefix}.$1`, 193 }, 194 { 195 paths: '*.{java,kt}', 196 find: /\b((System|SoLoader)\.loadLibrary\("[^"]*)("\);?)/g, 197 replaceWith: `$1_${prefix}$3`, 198 }, 199 { 200 paths: '*.{h,cpp}', 201 find: /(\bkJavaDescriptor\s*=\s*\n?\s*"L)(com\/)/gm, 202 replaceWith: `$1${prefix}/$2`, 203 }, 204 { 205 paths: 'build.gradle', 206 find: /\b(compileOnly|implementation|api)\s+['"]com.facebook.react:react-(native|android):?.*['"]/gm, 207 replaceWith: 208 `implementation 'host.exp:reactandroid-${prefix}:1.0.0'` + 209 '\n' + 210 // Adding some compile time common dependencies where the versioned react-native AAR doesn't expose 211 ` compileOnly 'com.facebook.fbjni:fbjni:+'\n` + 212 ` compileOnly 'com.facebook.yoga:proguard-annotations:+'\n` + 213 ` compileOnly 'com.facebook.soloader:soloader:+'\n` + 214 ` compileOnly 'com.facebook.fresco:fbcore:+'\n` + 215 ` compileOnly 'com.facebook.infer.annotation:infer-annotation:+'\n` + 216 ` compileOnly 'androidx.annotation:annotation:+'\n` + 217 ` compileOnly 'com.google.code.findbugs:jsr305:+'\n` + 218 ` compileOnly 'androidx.appcompat:appcompat:+'\n`, 219 }, 220 { 221 paths: ['build.gradle', 'CMakeLists.txt'], 222 find: /\/react-native\//g, 223 replaceWith: '/versioned-react-native/', 224 }, 225 { 226 paths: 'CMakeLists.txt', 227 find: /(^set\s*\(PACKAGE_NAME\s*['"])(\w+)(['"]\))/gm, 228 replaceWith: `$1$2_${prefix}$3`, 229 }, 230 { 231 paths: 'CMakeLists.txt', 232 find: /\b(ReactAndroid::[\w-]+)\b/g, 233 replaceWith: `$1_${prefix}`, 234 }, 235 { 236 paths: 'AndroidManifest.xml', 237 find: /(\bpackage=")([\w.]+)(")/, 238 replaceWith: `$1${prefix}.$2$3`, 239 }, 240 ], 241 }; 242} 243 244/** 245 * Returns the vendored directory for given SDK number. 246 */ 247function vendoredDirectoryForSDK(sdkNumber: number): string { 248 return path.join(ANDROID_VENDORED_DIR, `sdk${sdkNumber}`); 249} 250