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