1import spawnAsync from '@expo/spawn-async'; 2import fs from 'fs-extra'; 3import os from 'os'; 4import path from 'path'; 5 6import { EXPO_DIR, REACT_NATIVE_SUBMODULE_DIR } from '../../Constants'; 7import { GitDirectory } from '../../Git'; 8import logger from '../../Logger'; 9import { transformFilesAsync } from '../../Transforms'; 10import type { FileTransform } from '../../Transforms.types'; 11import { searchFilesAsync } from '../../Utils'; 12 13const TRANSFORM_HEADERS_API = ['hermes', 'DebuggerAPI']; 14const TRANSFORM_HEADERS_PUBLIC = [ 15 'RuntimeConfig', 16 'CrashManager', 17 'CtorConfig', 18 'DebuggerTypes', 19 'GCConfig', 20 'GCTripwireContext', 21]; 22 23const VERSIONED_JSI_DIR = 'versioned-jsi'; 24 25interface VersionHermesOptions { 26 // true to show verbose building log 27 verbose?: boolean; 28 29 // specify custom hermes download dir, use temp dir by default 30 hermesDir?: string; 31 32 // specify hermes git ref, use the version from *react-native-lab/react-native/sdks/.hermesversion* by default 33 hermesGitRef?: string; 34} 35 36function createHermesTransforms(versionName: string, versionedJsiDir: string): FileTransform[] { 37 return [ 38 { 39 find: /\b(facebook|hermes)::/g, 40 replaceWith: `${versionName}$1::`, 41 }, 42 { 43 find: /\bnamespace (facebook|hermes)/g, 44 replaceWith: `namespace ${versionName}$1`, 45 }, 46 { 47 find: /#include <jsi\/([^>]+)\.h>/g, 48 replaceWith: `#include <${versionName}jsi/${versionName}$1.h>`, 49 }, 50 { 51 find: /\b(HERMES_NON_CONSTEXPR|_HERMES_CTORCONFIG_)/g, 52 replaceWith: `${versionName}$1`, 53 }, 54 { 55 find: new RegExp( 56 `(#include ["<](hermes\\/)?)((${TRANSFORM_HEADERS_API.join('|')})\\.h[">])`, 57 'g' 58 ), 59 replaceWith: `$1${versionName}$3`, 60 }, 61 { 62 find: new RegExp( 63 `(#include ["<]hermes\\/Public\\/)((${TRANSFORM_HEADERS_PUBLIC.join('|')})\\.h[">])`, 64 'g' 65 ), 66 replaceWith: `$1${versionName}$2`, 67 }, 68 { 69 paths: `${VERSIONED_JSI_DIR}/${versionName}jsi/CMakeLists.txt`, 70 find: /\b(jsi\.cpp)\b/g, 71 replaceWith: `${versionName}$1`, 72 }, 73 { 74 paths: 'CMakeLists.txt', 75 find: 'add_subdirectory(${HERMES_JSI_DIR}/jsi ${CMAKE_CURRENT_BINARY_DIR}/jsi)', 76 replaceWith: `add_subdirectory(\${HERMES_JSI_DIR}/${versionName}jsi \${CMAKE_CURRENT_BINARY_DIR}/jsi)`, 77 }, 78 { 79 paths: 'utils/build-apple-framework.sh', 80 find: 'cmake -S . -B build_host_hermesc', 81 replaceWith: `cmake -S . -B build_host_hermesc -DJSI_DIR=${versionedJsiDir}`, 82 }, 83 { 84 // support specifying JSI_PATH by environment variable 85 paths: 'utils/build-apple-framework.sh', 86 find: 'JSI_PATH="$REACT_NATIVE_PATH/ReactCommon/jsi"', 87 replaceWith: 'JSI_PATH="${JSI_PATH:-$REACT_NATIVE_PATH/ReactCommon/jsi}"', 88 }, 89 // framework versioning 90 { 91 paths: 'API/hermes/CMakeLists.txt', 92 find: 'OUTPUT_NAME hermes', 93 replaceWith: `OUTPUT_NAME ${versionName}hermes`, 94 }, 95 { 96 paths: 'API/hermes/CMakeLists.txt', 97 find: 'MACOSX_FRAMEWORK_IDENTIFIER dev.hermesengine.', 98 // CFBundleIdentifier does not support underscores, replacing with hyphens. 99 // https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html 100 replaceWith: `MACOSX_FRAMEWORK_IDENTIFIER dev.${versionName.replace(/_/g, '-')}hermesengine.`, 101 }, 102 { 103 paths: 'utils/*.sh', 104 find: /\b(hermes.(xc)?framework)/g, 105 replaceWith: `${versionName}$1`, 106 }, 107 ]; 108} 109 110async function transformHermesAsync( 111 hermesRoot: string, 112 reactNativeRoot: string, 113 versionName: string 114) { 115 // use the build scripts from react-native to build hermes 116 await Promise.all( 117 [ 118 'utils/build-apple-framework.sh', 119 'utils/build-ios-framework.sh', 120 'utils/build-mac-framework.sh', 121 ].map((file) => 122 fs.copy( 123 path.join(REACT_NATIVE_SUBMODULE_DIR, 'sdks', 'hermes-engine', file), 124 path.join(hermesRoot, file) 125 ) 126 ) 127 ); 128 // workaround for 0.69 as it doesn't support specifying custom JSI path 129 await spawnAsync('wget', [ 130 '-O', 131 path.join(hermesRoot, 'utils/build-apple-framework.sh'), 132 'https://raw.githubusercontent.com/facebook/react-native/0.70-stable/sdks/hermes-engine/utils/build-apple-framework.sh', 133 ]); 134 135 // copy versioned jsi files from react-native 136 const versionedJsiDir = path.join(hermesRoot, VERSIONED_JSI_DIR); 137 await fs.copy(path.join(reactNativeRoot, 'ReactCommon', 'jsi'), versionedJsiDir); 138 await fs.rename( 139 path.join(versionedJsiDir, 'jsi'), 140 path.join(versionedJsiDir, `${versionName}jsi`) 141 ); 142 143 // transform content 144 const currDir = process.cwd(); 145 process.chdir(hermesRoot); // change cwd to hermesRoot for transformFilesAsync writing in relative path 146 const transformDirs = ['API', 'include', 'lib', 'public', 'tools', 'utils'].join(','); 147 const files = Array.from( 148 await searchFilesAsync(hermesRoot, [ 149 `{${transformDirs}}/**/*.{h,cpp,mm}`, 150 '**/CMakeLists.txt', 151 '**/*.sh', 152 ]) 153 ); 154 await transformFilesAsync(files, createHermesTransforms(versionName, versionedJsiDir)); 155 process.chdir(currDir); 156 157 // transform file names 158 await Promise.all( 159 TRANSFORM_HEADERS_API.map((file) => { 160 const dir = path.join(hermesRoot, 'API', 'hermes'); 161 return fs.move(path.join(dir, `${file}.h`), path.join(dir, `${versionName}${file}.h`)); 162 }) 163 ); 164 await Promise.all( 165 TRANSFORM_HEADERS_PUBLIC.map((file) => { 166 const dir = path.join(hermesRoot, 'public', 'hermes', 'Public'); 167 return fs.move(path.join(dir, `${file}.h`), path.join(dir, `${versionName}${file}.h`)); 168 }) 169 ); 170} 171 172function downloadHermesSourceAsync(downloadDir: string, ref: string) { 173 return GitDirectory.shallowCloneAsync(downloadDir, 'https://github.com/facebook/hermes.git', ref); 174} 175 176function buildHermesAsync(hermesRoot: string, options?: VersionHermesOptions) { 177 const versionedJsiDir = path.join(hermesRoot, VERSIONED_JSI_DIR); 178 return spawnAsync('./utils/build-ios-framework.sh', [], { 179 cwd: hermesRoot, 180 shell: true, 181 env: { 182 ...process.env, 183 JSI_PATH: versionedJsiDir, 184 }, 185 stdio: options?.verbose ? 'inherit' : 'ignore', 186 }); 187} 188 189async function removeUnusedHeaders(hermesRoot: string, versionName: string) { 190 const destRoot = path.join(hermesRoot, 'destroot'); 191 192 // remove jsi headers 193 await fs.remove(path.join(destRoot, 'include', 'jsi')); 194 195 // remove unused and unversioned headers 196 const files = Array.from( 197 await searchFilesAsync(path.join(destRoot, 'include', 'hermes'), [`**/!(${versionName})*`], { 198 absolute: true, 199 }) 200 ); 201 await Promise.all(files.map((file) => fs.remove(file))); 202} 203 204export async function createVersionedHermesTarball( 205 versionedReactNativeRoot: string, 206 versionName: string, 207 options?: VersionHermesOptions 208): Promise<string> { 209 const hermesGitRef = 210 options?.hermesGitRef ?? 211 (await fs.readFile(path.join(REACT_NATIVE_SUBMODULE_DIR, 'sdks', '.hermesversion'), 'utf8')); 212 if (!hermesGitRef) { 213 throw new Error('Cannot get bundled hermes version from react-native.'); 214 } 215 216 const hermesRoot = options?.hermesDir ?? path.join(os.tmpdir(), 'hermes'); 217 try { 218 await fs.remove(hermesRoot); 219 await fs.ensureDir(hermesRoot); 220 221 logger.log('Downloading hermes source code'); 222 await downloadHermesSourceAsync(hermesRoot, hermesGitRef); 223 224 logger.log('Versioning hermes source code'); 225 await transformHermesAsync(hermesRoot, versionedReactNativeRoot, versionName); 226 227 logger.log('Building hermes'); 228 await buildHermesAsync(hermesRoot, options); 229 230 const tarball = path.join(EXPO_DIR, `${versionName}hermes.tar.gz`); 231 logger.log(`Archiving hermes tarball: ${tarball}`); 232 await removeUnusedHeaders(hermesRoot, versionName); 233 // NOTE(kudo): we should include the _LICENSE_ file in the tarball, otherwise CocoaPods will get empty result from tarball extraction. 234 await spawnAsync('tar', ['cvfz', tarball, 'destroot', 'LICENSE'], { 235 cwd: hermesRoot, 236 stdio: options?.verbose ? 'inherit' : 'ignore', 237 }); 238 return tarball; 239 } finally { 240 await fs.remove(hermesRoot); 241 } 242} 243