1import type { ExpoConfig, Platform } from '@expo/config'; 2import spawnAsync from '@expo/spawn-async'; 3import fs from 'fs-extra'; 4import os from 'os'; 5import path from 'path'; 6import process from 'process'; 7 8import { 9 importMetroSourceMapComposeSourceMapsFromProject, 10 resolveFromProject, 11} from '../start/server/metro/resolveFromProject'; 12 13export function importHermesCommandFromProject(projectRoot: string): string { 14 const platformExecutable = getHermesCommandPlatform(); 15 const hermescLocations = [ 16 // Override hermesc dir by environment variables 17 process.env['REACT_NATIVE_OVERRIDE_HERMES_DIR'] 18 ? `${process.env['REACT_NATIVE_OVERRIDE_HERMES_DIR']}/build/bin/hermesc` 19 : '', 20 21 // Building hermes from source 22 'react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc', 23 24 // Prebuilt hermesc in official react-native 0.69+ 25 `react-native/sdks/hermesc/${platformExecutable}`, 26 27 // Legacy hermes-engine package 28 `hermes-engine/${platformExecutable}`, 29 ]; 30 31 for (const location of hermescLocations) { 32 try { 33 return resolveFromProject(projectRoot, location); 34 } catch {} 35 } 36 throw new Error('Cannot find the hermesc executable.'); 37} 38 39function getHermesCommandPlatform(): string { 40 switch (os.platform()) { 41 case 'darwin': 42 return 'osx-bin/hermesc'; 43 case 'linux': 44 return 'linux64-bin/hermesc'; 45 case 'win32': 46 return 'win64-bin/hermesc.exe'; 47 default: 48 throw new Error(`Unsupported host platform for Hermes compiler: ${os.platform()}`); 49 } 50} 51 52export function isEnableHermesManaged(expoConfig: ExpoConfig, platform: Platform): boolean { 53 switch (platform) { 54 case 'android': { 55 return (expoConfig.android?.jsEngine ?? expoConfig.jsEngine) !== 'jsc'; 56 } 57 case 'ios': { 58 return (expoConfig.ios?.jsEngine ?? expoConfig.jsEngine) !== 'jsc'; 59 } 60 default: 61 return false; 62 } 63} 64 65interface HermesBundleOutput { 66 hbc: Uint8Array; 67 sourcemap: string; 68} 69export async function buildHermesBundleAsync( 70 projectRoot: string, 71 code: string, 72 map: string, 73 optimize: boolean = false 74): Promise<HermesBundleOutput> { 75 const tempDir = path.join(os.tmpdir(), `expo-bundler-${process.pid}`); 76 await fs.ensureDir(tempDir); 77 try { 78 const tempBundleFile = path.join(tempDir, 'index.bundle'); 79 const tempSourcemapFile = path.join(tempDir, 'index.bundle.map'); 80 await fs.writeFile(tempBundleFile, code); 81 await fs.writeFile(tempSourcemapFile, map); 82 83 const tempHbcFile = path.join(tempDir, 'index.hbc'); 84 const hermesCommand = importHermesCommandFromProject(projectRoot); 85 const args = ['-emit-binary', '-out', tempHbcFile, tempBundleFile, '-output-source-map']; 86 if (optimize) { 87 args.push('-O'); 88 } 89 await spawnAsync(hermesCommand, args); 90 91 const [hbc, sourcemap] = await Promise.all([ 92 fs.readFile(tempHbcFile), 93 createHermesSourcemapAsync(projectRoot, map, `${tempHbcFile}.map`), 94 ]); 95 return { 96 hbc, 97 sourcemap, 98 }; 99 } finally { 100 await fs.remove(tempDir); 101 } 102} 103 104export async function createHermesSourcemapAsync( 105 projectRoot: string, 106 sourcemap: string, 107 hermesMapFile: string 108): Promise<string> { 109 const composeSourceMaps = importMetroSourceMapComposeSourceMapsFromProject(projectRoot); 110 const bundlerSourcemap = JSON.parse(sourcemap); 111 const hermesSourcemap = await fs.readJSON(hermesMapFile); 112 return JSON.stringify(composeSourceMaps([bundlerSourcemap, hermesSourcemap])); 113} 114 115export function parseGradleProperties(content: string): Record<string, string> { 116 const result: Record<string, string> = {}; 117 for (let line of content.split('\n')) { 118 line = line.trim(); 119 if (!line || line.startsWith('#')) { 120 continue; 121 } 122 123 const sepIndex = line.indexOf('='); 124 const key = line.substr(0, sepIndex); 125 const value = line.substr(sepIndex + 1); 126 result[key] = value; 127 } 128 return result; 129} 130 131export async function maybeThrowFromInconsistentEngineAsync( 132 projectRoot: string, 133 configFilePath: string, 134 platform: string, 135 isHermesManaged: boolean 136): Promise<void> { 137 const configFileName = path.basename(configFilePath); 138 if ( 139 platform === 'android' && 140 (await maybeInconsistentEngineAndroidAsync(projectRoot, isHermesManaged)) 141 ) { 142 throw new Error( 143 `JavaScript engine configuration is inconsistent between ${configFileName} and Android native project.\n` + 144 `In ${configFileName}: Hermes is ${isHermesManaged ? 'enabled' : 'not enabled'}\n` + 145 `In Android native project: Hermes is ${isHermesManaged ? 'not enabled' : 'enabled'}\n` + 146 `Please check the following files for inconsistencies:\n` + 147 ` - ${configFilePath}\n` + 148 ` - ${path.join(projectRoot, 'android', 'gradle.properties')}\n` + 149 ` - ${path.join(projectRoot, 'android', 'app', 'build.gradle')}\n` + 150 'Learn more: https://expo.fyi/hermes-android-config' 151 ); 152 } 153 154 if (platform === 'ios' && (await maybeInconsistentEngineIosAsync(projectRoot, isHermesManaged))) { 155 throw new Error( 156 `JavaScript engine configuration is inconsistent between ${configFileName} and iOS native project.\n` + 157 `In ${configFileName}: Hermes is ${isHermesManaged ? 'enabled' : 'not enabled'}\n` + 158 `In iOS native project: Hermes is ${isHermesManaged ? 'not enabled' : 'enabled'}\n` + 159 `Please check the following files for inconsistencies:\n` + 160 ` - ${configFilePath}\n` + 161 ` - ${path.join(projectRoot, 'ios', 'Podfile')}\n` + 162 ` - ${path.join(projectRoot, 'ios', 'Podfile.properties.json')}\n` + 163 'Learn more: https://expo.fyi/hermes-ios-config' 164 ); 165 } 166} 167 168export async function maybeInconsistentEngineAndroidAsync( 169 projectRoot: string, 170 isHermesManaged: boolean 171): Promise<boolean> { 172 // Trying best to check android native project if by chance to be consistent between app config 173 174 // Check gradle.properties from prebuild template 175 const gradlePropertiesPath = path.join(projectRoot, 'android', 'gradle.properties'); 176 if (fs.existsSync(gradlePropertiesPath)) { 177 const props = parseGradleProperties(await fs.readFile(gradlePropertiesPath, 'utf8')); 178 const isHermesBare = props['hermesEnabled'] === 'true'; 179 if (isHermesManaged !== isHermesBare) { 180 return true; 181 } 182 } 183 184 return false; 185} 186 187export async function maybeInconsistentEngineIosAsync( 188 projectRoot: string, 189 isHermesManaged: boolean 190): Promise<boolean> { 191 // Trying best to check ios native project if by chance to be consistent between app config 192 193 // Check ios/Podfile for ":hermes_enabled => true" 194 const podfilePath = path.join(projectRoot, 'ios', 'Podfile'); 195 if (fs.existsSync(podfilePath)) { 196 const content = await fs.readFile(podfilePath, 'utf8'); 197 const isPropsReference = 198 content.search( 199 /^\s*:hermes_enabled\s*=>\s*podfile_properties\['expo.jsEngine'\]\s*==\s*nil\s*\|\|\s*podfile_properties\['expo.jsEngine'\]\s*==\s*'hermes',?/m 200 ) >= 0; 201 const isHermesBare = content.search(/^\s*:hermes_enabled\s*=>\s*true,?\s+/m) >= 0; 202 if (!isPropsReference && isHermesManaged !== isHermesBare) { 203 return true; 204 } 205 } 206 207 // Check Podfile.properties.json from prebuild template 208 const podfilePropertiesPath = path.join(projectRoot, 'ios', 'Podfile.properties.json'); 209 if (fs.existsSync(podfilePropertiesPath)) { 210 const props = await parsePodfilePropertiesAsync(podfilePropertiesPath); 211 const isHermesBare = props['expo.jsEngine'] === 'hermes'; 212 if (isHermesManaged !== isHermesBare) { 213 return true; 214 } 215 } 216 217 return false; 218} 219 220// https://github.com/facebook/hermes/blob/release-v0.5/include/hermes/BCGen/HBC/BytecodeFileFormat.h#L24-L25 221const HERMES_MAGIC_HEADER = 'c61fbc03c103191f'; 222 223export async function isHermesBytecodeBundleAsync(file: string): Promise<boolean> { 224 const header = await readHermesHeaderAsync(file); 225 return header.slice(0, 8).toString('hex') === HERMES_MAGIC_HEADER; 226} 227 228export async function getHermesBytecodeBundleVersionAsync(file: string): Promise<number> { 229 const header = await readHermesHeaderAsync(file); 230 if (header.slice(0, 8).toString('hex') !== HERMES_MAGIC_HEADER) { 231 throw new Error('Invalid hermes bundle file'); 232 } 233 return header.readUInt32LE(8); 234} 235 236async function readHermesHeaderAsync(file: string): Promise<Buffer> { 237 const fd = await fs.open(file, 'r'); 238 const buffer = Buffer.alloc(12); 239 await fs.read(fd, buffer, 0, 12, null); 240 await fs.close(fd); 241 return buffer; 242} 243 244async function parsePodfilePropertiesAsync( 245 podfilePropertiesPath: string 246): Promise<Record<string, string>> { 247 try { 248 return JSON.parse(await fs.readFile(podfilePropertiesPath, 'utf8')); 249 } catch { 250 return {}; 251 } 252} 253