1import spawnAsync from '@expo/spawn-async';
2import fs from 'fs-extra';
3import path from 'path';
4
5import { copyFileWithTransformsAsync, transformFileAsync } from '../../Transforms';
6import { searchFilesAsync } from '../../Utils';
7import {
8  codegenTransforms,
9  hermesTransforms,
10  reactNativeTransforms,
11} from './reactNativeTransforms';
12
13export async function updateVersionedReactNativeAsync(
14  reactNativeSubmoduleRoot: string,
15  androidDir: string,
16  sdkVersion: string
17): Promise<void> {
18  const abiVersion = `abi${sdkVersion.replace(/\./g, '_')}`;
19  const versionedReactNativeDir = path.join(androidDir, 'versioned-react-native');
20  await Promise.all([
21    fs.remove(path.join(versionedReactNativeDir, 'ReactAndroid')),
22    fs.remove(path.join(versionedReactNativeDir, 'ReactCommon')),
23    fs.remove(path.join(versionedReactNativeDir, 'codegen')),
24    fs.remove(path.join(versionedReactNativeDir, 'sdks')),
25  ]);
26
27  await fs.mkdirp(path.join(versionedReactNativeDir, 'sdks'));
28  await fs.copy(
29    path.join(androidDir, 'sdks/.hermesversion'),
30    path.join(versionedReactNativeDir, 'sdks/.hermesversion')
31  );
32
33  // Run and version codegen
34  const codegenOutputRoot = path.join(versionedReactNativeDir, 'codegen');
35  const tmpCodegenOutputRoot = path.join(versionedReactNativeDir, 'codegen-tmp');
36  try {
37    await runReactNativeCodegenAndroidAsync(reactNativeSubmoduleRoot, tmpCodegenOutputRoot);
38    await versionCodegenDirectoryAsync(tmpCodegenOutputRoot, codegenOutputRoot, abiVersion);
39  } finally {
40    await fs.remove(tmpCodegenOutputRoot);
41  }
42
43  // Copy and version ReactAndroid and ReactCommon
44  await versionReactNativeAsync(androidDir, versionedReactNativeDir, abiVersion);
45
46  await versionHermesAsync(versionedReactNativeDir, abiVersion);
47}
48
49async function versionHermesAsync(versionedReactNativeDir: string, abiVersion: string) {
50  await spawnAsync('./gradlew', [':ReactAndroid:hermes-engine:unzipHermes'], {
51    shell: true,
52    cwd: versionedReactNativeDir,
53    stdio: 'inherit',
54  });
55  await transformFileAsync(
56    path.join(versionedReactNativeDir, 'sdks/hermes/API/hermes/CMakeLists.txt'),
57    hermesTransforms(abiVersion)
58  );
59}
60
61async function versionReactNativeAsync(
62  androidDir: string,
63  versionedReactNativeDir: string,
64  abiVersion: string
65) {
66  const files = await searchFilesAsync(androidDir, ['./ReactAndroid/**', './ReactCommon/**']);
67  for (const file of files) {
68    if ((file.match(/\/build\//) && !file.match(/src.*\/build\//)) || file.match(/\/\.cxx\//)) {
69      files.delete(file);
70    }
71  }
72
73  const transforms = reactNativeTransforms(versionedReactNativeDir, abiVersion);
74  for (const sourceFile of files) {
75    await copyFileWithTransformsAsync({
76      sourceFile,
77      targetDirectory: versionedReactNativeDir,
78      sourceDirectory: androidDir,
79      transforms,
80    });
81  }
82}
83
84async function versionCodegenDirectoryAsync(
85  tmpCodegenDir: string,
86  codegenDir: string,
87  abiVersion: string
88) {
89  const files = await searchFilesAsync(tmpCodegenDir, ['**']);
90  const transforms = codegenTransforms(abiVersion);
91  for (const sourceFile of files) {
92    await copyFileWithTransformsAsync({
93      sourceFile,
94      targetDirectory: codegenDir,
95      sourceDirectory: tmpCodegenDir,
96      transforms,
97    });
98  }
99}
100
101async function runReactNativeCodegenAndroidAsync(
102  reactNativeSubmoduleRoot: string,
103  tmpCodegenOutputRoot: string
104) {
105  await fs.remove(tmpCodegenOutputRoot);
106  await fs.ensureDir(tmpCodegenOutputRoot);
107
108  // generate schema.json from js & flow types
109  const genSchemaScript = path.join(
110    reactNativeSubmoduleRoot,
111    'packages',
112    'react-native-codegen',
113    'lib',
114    'cli',
115    'combine',
116    'combine-js-to-schema-cli.js'
117  );
118  const schemaOutputPath = path.join(tmpCodegenOutputRoot, 'schema.json');
119  const jsSourceRoot = path.join(reactNativeSubmoduleRoot, 'Libraries');
120  await spawnAsync('yarn', ['node', genSchemaScript, schemaOutputPath, jsSourceRoot]);
121
122  // generate code from schema.json
123  const genCodeScript = path.join(reactNativeSubmoduleRoot, 'scripts', 'generate-specs-cli.js');
124  await spawnAsync('yarn', [
125    'node',
126    genCodeScript,
127    '--platform',
128    'android',
129    '--schemaPath',
130    schemaOutputPath,
131    '--outputDir',
132    tmpCodegenOutputRoot,
133    '--libraryName',
134    'rncore',
135    '--javaPackageName',
136    'com.facebook.fbreact.specs',
137  ]);
138}
139
140export async function renameHermesEngine(versionedReactAndroidPath: string, version: string) {
141  const abiVersion = version.replace(/\./g, '_');
142  const abiName = `abi${abiVersion}`;
143  const prebuiltHermesMkPath = path.join(
144    versionedReactAndroidPath,
145    'src',
146    'main',
147    'jni',
148    'first-party',
149    'hermes',
150    'Android.mk'
151  );
152  const versionedHermesLibName = `libhermes_${abiName}.so`;
153  await transformFileAsync(prebuiltHermesMkPath, [
154    {
155      find: /^(LOCAL_SRC_FILES\s+:=\s+jni\/\$\(TARGET_ARCH_ABI\))\/libhermes.so$/gm,
156      replaceWith: `$1/${versionedHermesLibName}`,
157    },
158  ]);
159
160  const buildGradlePath = path.join(versionedReactAndroidPath, 'build.gradle');
161  // patch prepareHermes task to rename copied library and update soname
162  // the diff is something like that:
163  //
164  // ```diff
165  // --- android/versioned-react-native/ReactAndroid/build.gradle.orig       2021-08-14 00:40:18.000000000 +0800
166  // +++ android/versioned-react-native/ReactAndroid/build.gradle    2021-08-14 00:40:58.000000000 +0800
167  // @@ -114,7 +114,7 @@
168  //      into("$thirdPartyNdkDir/folly")
169  //  }
170  //
171  // -task prepareHermes(dependsOn: createNativeDepsDirectories, type: Copy) {
172  // +task prepareHermes(dependsOn: createNativeDepsDirectories) {
173  //      def hermesPackagePath = findNodeModulePath(projectDir, "hermes-engine")
174  //      if (!hermesPackagePath) {
175  //          throw new GradleScriptException("Could not find the hermes-engine npm package", null)
176  // @@ -126,12 +126,29 @@
177  //      }
178  //
179  //      def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })
180  // -
181  // +    copy {
182  // +
183  //      from soFiles
184  //      from "src/main/jni/first-party/hermes/Android.mk"
185  //      into "$thirdPartyNdkDir/hermes"
186  // +
187  // +        rename '(.+).so', '$1_abi43_0_0.so'
188  // +    }
189  // +    exec {
190  // +        commandLine("patchelf", "--set-soname", "libhermes_abi43_0_0.so", "$thirdPartyNdkDir/hermes/jni/arm64-v8a/libhermes_abi43_0_0.so")
191  // +    }
192  // +    exec {
193  // +        commandLine("patchelf", "--set-soname", "libhermes_abi43_0_0.so", "$thirdPartyNdkDir/hermes/jni/armeabi-v7a/libhermes_abi43_0_0.so")
194  // +    }
195  // +    exec {
196  // +        commandLine("patchelf", "--set-soname", "libhermes_abi43_0_0.so", "$thirdPartyNdkDir/hermes/jni/x86/libhermes_abi43_0_0.so")
197  // +    }
198  // +    exec {
199  // +        commandLine("patchelf", "--set-soname", "libhermes_abi43_0_0.so", "$thirdPartyNdkDir/hermes/jni/x86_64/libhermes_abi43_0_0.so")
200  // +    }
201  //  }
202  //
203  // +
204  //  task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
205  //      src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
206  //      onlyIfNewer(true)
207  // ```
208  await transformFileAsync(buildGradlePath, [
209    {
210      // reset `prepareHermes` task from Copy type to generic type then we can do both copy and exec.
211      find: /^(task prepareHermes\(dependsOn: .+), type: Copy(\).+$)/m,
212      replaceWith: '$1$2',
213    },
214    {
215      // wrap copy task and append exec tasks
216      find: /(^\s*def soFiles = zipTree\(hermesAAR\).+)\n([\s\S]+?)^\}/gm,
217      replaceWith: `\
218$1
219    copy {
220        $2
221        rename '(.+).so', '$$1_abi${abiVersion}.so'
222    }
223    exec {
224        commandLine("patchelf", "--set-soname", "${versionedHermesLibName}", "$thirdPartyNdkDir/hermes/jni/arm64-v8a/${versionedHermesLibName}")
225    }
226    exec {
227        commandLine("patchelf", "--set-soname", "${versionedHermesLibName}", "$thirdPartyNdkDir/hermes/jni/armeabi-v7a/${versionedHermesLibName}")
228    }
229    exec {
230        commandLine("patchelf", "--set-soname", "${versionedHermesLibName}", "$thirdPartyNdkDir/hermes/jni/x86/${versionedHermesLibName}")
231    }
232    exec {
233        commandLine("patchelf", "--set-soname", "${versionedHermesLibName}", "$thirdPartyNdkDir/hermes/jni/x86_64/${versionedHermesLibName}")
234    }
235}
236`,
237    },
238  ]);
239}
240