Skip to content

Commit 3c6fae7

Browse files
committed
Fix Hermes compiler detection for monorepos
1 parent 94380cb commit 3c6fae7

3 files changed

Lines changed: 113 additions & 22 deletions

File tree

packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/tasks/BundleHermesCTask.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,12 @@ abstract class BundleHermesCTask : DefaultTask() {
9494
runCommand(bundleCommand)
9595

9696
if (hermesEnabled.get()) {
97-
val detectedHermesCommand = detectOSAwareHermesCommand(root.get().asFile, hermesCommand.get())
97+
val detectedHermesCommand =
98+
detectOSAwareHermesCommand(
99+
root.get().asFile,
100+
hermesCommand.get(),
101+
reactNativeDir.get().asFile,
102+
)
98103
val bytecodeFile = File("${bundleFile}.hbc")
99104
val outputSourceMap = resolveOutputSourceMap(bundleAssetFilename)
100105
val compilerSourceMap = resolveCompilerSourceMap(bundleAssetFilename)

packages/gradle-plugin/react-native-gradle-plugin/src/main/kotlin/com/facebook/react/utils/PathUtils.kt

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ internal fun detectedCliFile(config: ReactExtension): File =
5757
* 4. Fails otherwise
5858
*/
5959
internal fun detectedHermesCommand(config: ReactExtension): String =
60-
detectOSAwareHermesCommand(config.root.get().asFile, config.hermesCommand.get())
60+
detectOSAwareHermesCommand(
61+
config.root.get().asFile,
62+
config.hermesCommand.get(),
63+
config.reactNativeDir.get().asFile,
64+
)
6165

6266
private fun detectEntryFile(
6367
entryFile: File?,
@@ -120,16 +124,20 @@ private fun detectCliFile(
120124
/**
121125
* Computes the `hermesc` command location. The Algo follows this order:
122126
* 1. The path provided by the `hermesCommand` config in the `react` Gradle extension
123-
* 2. The file located in `node_modules/react-native/sdks/hermes/build/bin/hermesc`. This will be
127+
* 2. The file located in
128+
* `node_modules/react-native/ReactAndroid/hermes-engine/build/hermes/bin/hermesc`. This will be
124129
* used if the user is building Hermes from source.
125-
* 3. The file located in `node_modules/hermes-compiler/%OS-BIN%/hermesc` where `%OS-BIN%` is
126-
* substituted with the correct OS arch. This is used when Hermes V1 is consumed as a prebuilt
130+
* 3. The built from source hermesc located under the configured `reactNativeDir`.
131+
* 4. The file located in `node_modules/hermes-compiler/hermesc/%OS-BIN%/hermesc` where `%OS-BIN%` is
132+
* substituted with the correct OS arch. This is used when Hermes is consumed as a prebuilt
127133
* package via the `hermes-compiler` npm package.
128-
* 4. Fails otherwise
134+
* 5. The prebuilt `hermes-compiler` package located next to the configured `reactNativeDir`.
135+
* 6. Fails otherwise
129136
*/
130137
internal fun detectOSAwareHermesCommand(
131138
projectRoot: File,
132139
hermesCommand: String,
140+
reactNativeDir: File? = null,
133141
): String { // 1. If the project specifies a Hermes command, don't second guess it.
134142
if (hermesCommand.isNotBlank()) {
135143
val osSpecificHermesCommand =
@@ -144,31 +152,52 @@ internal fun detectOSAwareHermesCommand(
144152
}
145153

146154
// 2. If the project is building hermes-engine from source, use hermesc from there
147-
val builtHermesc =
148-
getBuiltHermescFile(projectRoot, System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR"))
149-
if (builtHermesc.exists()) {
150-
return builtHermesc.cliPath(projectRoot)
151-
}
155+
val builtHermescCandidates =
156+
listOfNotNull(
157+
getBuiltHermescFile(projectRoot, System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR")),
158+
reactNativeDir?.let { getBuiltHermescFileFromReactNativeDir(it) },
159+
)
160+
builtHermescCandidates.firstOrNull { it.exists() }?.let { return it.cliPath(projectRoot) }
161+
162+
val prebuiltHermesCandidates = getHermesCompilerCandidates(projectRoot, reactNativeDir)
163+
prebuiltHermesCandidates.firstOrNull { it.exists() }?.let { return it.cliPath(projectRoot) }
164+
165+
error(
166+
buildString {
167+
appendLine("Couldn't determine Hermesc location.")
168+
appendLine("Checked:")
169+
(builtHermescCandidates + prebuiltHermesCandidates).forEach { appendLine("- ${it.path}") }
170+
append(
171+
"Please set `react.hermesCommand` to the path of the hermesc binary file, " +
172+
"or make sure the `hermes-compiler` package is installed and resolvable from " +
173+
"your project root or configured `reactNativeDir`."
174+
)
175+
}
176+
)
177+
}
152178

153-
// 3. Use hermes-compiler from npm
179+
private fun getHermesCompilerCandidates(projectRoot: File, reactNativeDir: File?): List<File> {
154180
val prebuiltHermesPath =
155181
HERMES_COMPILER_NPM_DIR.plus(getHermesCBin())
156182
.replace("%OS-BIN%", getHermesOSBin())
157183
// Execution on Windows fails with / as separator
158184
.replace('/', File.separatorChar)
159185

160-
val prebuiltHermes = File(projectRoot, prebuiltHermesPath)
161-
if (prebuiltHermes.exists()) {
162-
return prebuiltHermes.cliPath(projectRoot)
163-
}
164-
165-
error(
166-
"Couldn't determine Hermesc location. " +
167-
"Please set `react.hermesCommand` to the path of the hermesc binary file. " +
168-
"node_modules/react-native/sdks/hermesc/%OS-BIN%/hermesc"
169-
)
186+
return listOfNotNull(
187+
File(projectRoot, prebuiltHermesPath),
188+
reactNativeDir?.parentFile?.let { nodeModulesDir ->
189+
File(nodeModulesDir, prebuiltHermesPath.removePrefix("node_modules${File.separator}"))
190+
},
191+
)
192+
.distinctBy { it.absolutePath }
170193
}
171194

195+
private fun getBuiltHermescFileFromReactNativeDir(reactNativeDir: File): File =
196+
File(
197+
reactNativeDir,
198+
"ReactAndroid/hermes-engine/build/hermes/bin/${getHermesCBin()}",
199+
)
200+
172201
/**
173202
* Gets the location where Hermesc should be. If nothing is specified, built hermesc is assumed to
174203
* be inside [HERMESC_BUILT_FROM_SOURCE_DIR]. Otherwise user can specify an override with

packages/gradle-plugin/react-native-gradle-plugin/src/test/kotlin/com/facebook/react/utils/PathUtilsTest.kt

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.facebook.react.tests.OsRule
1414
import com.facebook.react.tests.WithOs
1515
import java.io.File
1616
import org.assertj.core.api.Assertions.assertThat
17+
import org.assertj.core.api.Assertions.assertThatThrownBy
1718
import org.gradle.process.ProcessExecutionException
1819
import org.gradle.testfixtures.ProjectBuilder
1920
import org.junit.Assume.assumeTrue
@@ -163,12 +164,68 @@ class PathUtilsTest {
163164
assertThat(detectOSAwareHermesCommand(tempFolder.root, "")).isEqualTo(expected.toString())
164165
}
165166

167+
@Test
168+
@WithOs(OS.MAC)
169+
fun detectOSAwareHermesCommand_withHoistedHermescFromNPM() {
170+
val projectRoot = tempFolder.newFolder("apps", "mobile")
171+
val reactNativeDir = tempFolder.newFolder("node_modules", "react-native")
172+
tempFolder.newFolder("node_modules/hermes-compiler/hermesc/osx-bin/")
173+
val expected = tempFolder.newFile("node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
174+
175+
assertThat(detectOSAwareHermesCommand(projectRoot, "", reactNativeDir))
176+
.isEqualTo(expected.toString())
177+
}
178+
179+
@Test
180+
@WithOs(OS.MAC)
181+
fun detectOSAwareHermesCommand_withLocalAndHoistedHermescFromNPM_prefersLocal() {
182+
val projectRoot = tempFolder.newFolder("apps", "mobile")
183+
val reactNativeDir = tempFolder.newFolder("node_modules", "react-native")
184+
File(projectRoot, "node_modules/hermes-compiler/hermesc/osx-bin/").mkdirs()
185+
val expected = File(projectRoot, "node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
186+
expected.createNewFile()
187+
tempFolder.newFolder("node_modules/hermes-compiler/hermesc/osx-bin/")
188+
tempFolder.newFile("node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
189+
190+
assertThat(detectOSAwareHermesCommand(projectRoot, "", reactNativeDir))
191+
.isEqualTo(expected.toString())
192+
}
193+
194+
@Test
195+
fun detectedHermesCommand_usesConfiguredReactNativeDirForHoistedHermescFromNPM() {
196+
val project = ProjectBuilder.builder().build()
197+
val extension = TestReactExtension(project)
198+
val projectRoot = tempFolder.newFolder("apps", "mobile")
199+
val reactNativeDir = tempFolder.newFolder("node_modules", "react-native")
200+
extension.root.set(projectRoot)
201+
extension.reactNativeDir.set(reactNativeDir)
202+
tempFolder.newFolder("node_modules/hermes-compiler/hermesc/${getHermesOSBin()}/")
203+
val expected = tempFolder.newFile("node_modules/hermes-compiler/hermesc/${getHermesOSBin()}/hermesc")
204+
205+
assertThat(detectedHermesCommand(extension)).isEqualTo(expected.toString())
206+
}
207+
166208
@Test(expected = IllegalStateException::class)
167209
@WithOs(OS.MAC)
168210
fun detectOSAwareHermesCommand_failsIfNotFound() {
169211
detectOSAwareHermesCommand(tempFolder.root, "")
170212
}
171213

214+
@Test
215+
@WithOs(OS.MAC)
216+
fun detectOSAwareHermesCommand_whenHermescNotFound_reportsCheckedLocations() {
217+
val projectRoot = tempFolder.newFolder("apps", "mobile")
218+
val reactNativeDir = tempFolder.newFolder("node_modules", "react-native")
219+
220+
assertThatThrownBy { detectOSAwareHermesCommand(projectRoot, "", reactNativeDir) }
221+
.isInstanceOf(IllegalStateException::class.java)
222+
.hasMessageContaining("Couldn't determine Hermesc location.")
223+
.hasMessageContaining("Checked:")
224+
.hasMessageContaining("node_modules/hermes-compiler/hermesc/osx-bin/hermesc")
225+
.hasMessageContaining("configured `reactNativeDir`")
226+
.hasMessageNotContaining("node_modules/react-native/sdks/hermesc")
227+
}
228+
172229
@Test
173230
@WithOs(OS.MAC)
174231
fun detectOSAwareHermesCommand_withProvidedCommand_takesPrecedence() {

0 commit comments

Comments
 (0)