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