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