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