Skip to content

Commit

Permalink
Build Hermes from Source (#33396)
Browse files Browse the repository at this point in the history
Summary:
Pull Request resolved: #33396

This commit fully unplugs the `ReactAndroid` from using hermes from the NPM package and plugs the usage of Hermes via the `packages/hermes-engine` Gradle build.

I've used prefab to share the .so between the two builds, so we don't need any extra machinery to make this possible.

Moreover, I've added a `buildHermesFromSource` property, which defaults to false when RN is imported, but is set to true when RN is opened for local development. This should allow us to distribute the `react-native` NPM package and users could potentially toggle which source to use (but see below).

Changelog:
[Android] [Changed] - Build Hermes from Source

Reviewed By: hramos

Differential Revision: D34389875

fbshipit-source-id: 107cbe3686daf7607a1f0f75202f24cd80ce64bb
  • Loading branch information
cortinico authored and facebook-github-bot committed Mar 11, 2022
1 parent d34a75e commit a3d9892
Show file tree
Hide file tree
Showing 16 changed files with 134 additions and 135 deletions.
6 changes: 2 additions & 4 deletions .circleci/Dockerfiles/Dockerfile.android
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
# and build a Android application that can be used to run the
# tests specified in the scripts/ directory.
#
FROM reactnativecommunity/react-native-android:5.2
FROM reactnativecommunity/react-native-android:5.4

LABEL Description="React Native Android Test Image"
LABEL maintainer="Héctor Ramos <[email protected]>"
Expand Down Expand Up @@ -53,6 +53,4 @@ ADD . /app

RUN yarn

RUN ./gradlew :ReactAndroid:downloadBoost :ReactAndroid:downloadDoubleConversion :ReactAndroid:downloadFolly :ReactAndroid:downloadGlog

RUN ./gradlew :ReactAndroid:packageReactNdkLibsForBuck -Pjobs=1
RUN ./gradlew :ReactAndroid:assembleDebug
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ executors:
reactnativeandroid:
<<: *defaults
docker:
- image: reactnativecommunity/react-native-android:5.2
- image: reactnativecommunity/react-native-android:5.4
resource_class: "large"
environment:
- TERM: "dumb"
Expand Down
47 changes: 20 additions & 27 deletions ReactAndroid/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ def boostPath = dependenciesPath ?: System.getenv("REACT_NATIVE_BOOST_PATH")
// Setup build type for NDK, supported values: {debug, release}
def nativeBuildType = System.getenv("NATIVE_BUILD_TYPE") ?: "release"

// We put the publishing version from gradle.properties inside ext. so other
// subprojects can access it as well.
ext.publishing_version = VERSION_NAME

task createNativeDepsDirectories {
downloadsDir.mkdirs()
thirdPartyNdkDir.mkdirs()
Expand Down Expand Up @@ -127,24 +131,6 @@ final def prepareLibevent = tasks.register("prepareLibevent", PrepareLibeventTas
it.outputDir.set(new File(thirdPartyNdkDir, "libevent"))
}

task prepareHermes(dependsOn: createNativeDepsDirectories, type: Copy) {
def hermesPackagePath = findNodeModulePath(projectDir, "hermes-engine")
if (!hermesPackagePath) {
throw new GradleScriptException("Could not find the hermes-engine npm package", null)
}

def hermesAAR = file("$hermesPackagePath/android/hermes-debug.aar")
if (!hermesAAR.exists()) {
throw new GradleScriptException("The hermes-engine npm package is missing \"android/hermes-debug.aar\"", null)
}

def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" })

from soFiles
from "src/main/jni/first-party/hermes/Android.mk"
into "$thirdPartyNdkDir/hermes"
}

task downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) {
src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz")
onlyIfNewer(true)
Expand Down Expand Up @@ -259,18 +245,11 @@ final def extractNativeDependencies = tasks.register('extractNativeDependencies'
it.extractHeadersConfiguration.setFrom(configurations.extractHeaders)
it.extractJniConfiguration.setFrom(configurations.extractJNI)
it.baseOutputDir = project.file("src/main/jni/first-party/")
// Sadly this task as an output folder path that is directly dependent on
// the task input (i.e. src/main/jni/first-party/<package-name>/...
// This means that this task is using the parent folder (first-party/) as
// @OutputFolder. The `prepareHermes` task will also output inside that
// folder and if the two tasks happen to be inside the same run, we want
// `extractNativeDependencies` to run after `prepareHermes` to do not
// invalidate the input/output calculation for this task.
it.mustRunAfter(prepareHermes)
}

task installArchives {
dependsOn("publishReleasePublicationToNpmRepository")
dependsOn(":ReactAndroid:hermes-engine:installArchives")
}

// Creating sources with comments
Expand Down Expand Up @@ -314,6 +293,7 @@ android {
"REACT_COMMON_DIR=$projectDir/../ReactCommon",
"REACT_GENERATED_SRC_DIR=$buildDir/generated/source",
"REACT_SRC_DIR=$projectDir/src/main/java/com/facebook/react",
"APP_STL=c++_shared",
"-j${ndkBuildJobs()}"

if (Os.isFamily(Os.FAMILY_MAC)) {
Expand All @@ -333,7 +313,7 @@ android {
}
}

preBuild.dependsOn(prepareJSC, prepareHermes, prepareBoost, prepareDoubleConversion, prepareFmt, prepareFolly, prepareGlog, prepareLibevent, extractNativeDependencies)
preBuild.dependsOn(prepareJSC, prepareBoost, prepareDoubleConversion, prepareFmt, prepareFolly, prepareGlog, prepareLibevent, extractNativeDependencies)
preBuild.dependsOn("generateCodegenArtifactsFromSchema")

sourceSets.main {
Expand Down Expand Up @@ -366,6 +346,10 @@ android {
extractJNI
javadocDeps.extendsFrom api
}

buildFeatures {
prefab true
}
}

dependencies {
Expand All @@ -388,6 +372,15 @@ dependencies {
extractHeaders("com.facebook.fbjni:fbjni:0.2.2:headers")
extractJNI("com.facebook.fbjni:fbjni:0.2.2")

// It's up to the consumer to decide if hermes should be included or not.
// Therefore hermes-engine is a compileOnly dependency.
// Moreover, you can toggle where to get the dependency with `buildHermesFromSource`.
if (project.getProperties().getOrDefault("buildHermesFromSource", "false").toBoolean()) {
compileOnly(project(":ReactAndroid:hermes-engine"))
} else {
compileOnly("com.facebook.react:hermes-engine:${VERSION_NAME}")
}

javadocDeps("com.squareup:javapoet:1.13.0")

testImplementation("junit:junit:${JUNIT_VERSION}")
Expand Down
3 changes: 3 additions & 0 deletions ReactAndroid/hermes-engine/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Make sure we never publish the build folders to npm.
build/
.cxx/
36 changes: 33 additions & 3 deletions ReactAndroid/hermes-engine/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ if (hermesVersionFile.exists()) {
hermesVersion = hermesVersionFile.text
}
def ndkBuildJobs = Runtime.runtime.availableProcessors().toString()
def prefabHeadersDir = new File("$buildDir/prefab-headers")

// We inject the JSI directory used inside the Hermes build with the -DJSI_DIR config.
def jsiDir = rootProject.file("ReactCommon/jsi")
Expand All @@ -42,6 +43,10 @@ task downloadNdkBuildDependencies() {
dependsOn(downloadHermes)
}

task installArchives {
dependsOn("publishAllPublicationsToNpmRepository")
}

task unzipHermes(dependsOn: downloadHermes, type: Copy) {
from(tarTree(downloadHermes.dest)) {
eachFile { file ->
Expand All @@ -56,7 +61,7 @@ task unzipHermes(dependsOn: downloadHermes, type: Copy) {
}


task configureNinjaForHermes(dependsOn: unzipHermes, type: org.gradle.api.tasks.Exec) {
task configureNinjaForHermes(dependsOn: unzipHermes, type: Exec) {
workingDir(hermesDir)
commandLine(windowsAwareCommandLine(
"python3",
Expand All @@ -67,11 +72,19 @@ task configureNinjaForHermes(dependsOn: unzipHermes, type: org.gradle.api.tasks.
))
}

task buildNinjaForHermes(dependsOn: configureNinjaForHermes, type: org.gradle.api.tasks.Exec) {
task buildNinjaForHermes(dependsOn: configureNinjaForHermes, type: Exec) {
workingDir(hermesDir)
commandLine(windowsAwareCommandLine("cmake", "--build", "./ninja_build", "--target", "hermesc", "-j", ndkBuildJobs))
}

task prepareHeadersForPrefab(dependsOn: unzipHermes, type: Copy) {
from("$hermesDir/API")
from("$hermesDir/public")
include("**/*.h")
exclude("jsi/**")
into(prefabHeadersDir)
}

static def windowsAwareCommandLine(String... commands) {
def newCommands = []
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
Expand All @@ -88,6 +101,7 @@ def reactNativeArchitectures() {

android {
compileSdkVersion 31
buildToolsVersion = "31.0.0"

// Used to override the NDK path & version on internal CI
if (System.getenv("ANDROID_NDK") != null && System.getenv("LOCAL_ANDROID_NDK_VERSION") != null) {
Expand Down Expand Up @@ -180,12 +194,28 @@ android {
allVariants()
}
}

buildFeatures {
prefabPublishing true
}

prefab {
libhermes {
headers prefabHeadersDir.absolutePath
libraryName "libhermes"
}
}
}

afterEvaluate {
preBuild.dependsOn(buildNinjaForHermes)
preBuild.dependsOn(prepareHeadersForPrefab)
// Needed as some of the native sources needs to be downloaded
// before configureCMakeRelease/configureCMakeMinSizeRel could be executed.
reactNativeArchitectures().each { architecture ->
tasks.named("configureCMakeMinSizeRel[${architecture}]") { dependsOn(preBuild) }
tasks.named("configureCMakeRelease[${architecture}]") { dependsOn(preBuild) }
}
configureCMakeRelease.dependsOn(preBuild)
configureCMakeMinSizeRel.dependsOn(preBuild)

Expand All @@ -206,4 +236,4 @@ afterEvaluate {
}

group = "com.facebook.react"
version = VERSION_NAME
version = parent.publishing_version
3 changes: 1 addition & 2 deletions ReactAndroid/hermes-engine/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
VERSION_NAME=1000.0.0-main
android.disableAutomaticComponentCreation=true
android.disableAutomaticComponentCreation=true
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,13 @@
LOCAL_PATH := $(call my-dir)
REACT_NATIVE := $(LOCAL_PATH)/../../../../../../../..

include $(REACT_NATIVE)/ReactCommon/common.mk
include $(CLEAR_VARS)

LOCAL_MODULE := jsijniprofiler

LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)

LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi $(call find-node-module,$(LOCAL_PATH),hermes-engine)/android/include
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi

LOCAL_CPP_FEATURES := exceptions

Expand All @@ -29,5 +28,3 @@ LOCAL_SHARED_LIBRARIES := \

include $(BUILD_SHARED_LIBRARY)


include $(CLEAR_VARS)
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,47 @@
LOCAL_PATH := $(call my-dir)
REACT_NATIVE := $(LOCAL_PATH)/../../../../../../../..

include $(REACT_NATIVE)/ReactCommon/common.mk
include $(CLEAR_VARS)
ifeq ($(APP_OPTIM),debug)
include $(CLEAR_VARS)

LOCAL_MODULE := hermes-executor-release
LOCAL_MODULE := hermes-executor-debug
LOCAL_CFLAGS := -DHERMES_ENABLE_DEBUGGER=1

LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)

LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi $(call find-node-module,$(LOCAL_PATH),hermes-engine)/android/include
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi

LOCAL_CPP_FEATURES := exceptions
LOCAL_CPP_FEATURES := exceptions

LOCAL_STATIC_LIBRARIES := libjsireact libhermes-executor-common-release
LOCAL_SHARED_LIBRARIES := \
libfb \
libfbjni \
libfolly_json \
libhermes \
libjsi \
libreactnativejni
LOCAL_STATIC_LIBRARIES := libjsireact libhermes-executor-common-debug
LOCAL_SHARED_LIBRARIES := \
libfb \
libfbjni \
libfolly_json \
libhermes \
libjsi \
libreactnativejni

include $(BUILD_SHARED_LIBRARY)
include $(BUILD_SHARED_LIBRARY)
else
include $(CLEAR_VARS)

LOCAL_MODULE := hermes-executor-release

include $(CLEAR_VARS)
LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)

LOCAL_MODULE := hermes-executor-debug
LOCAL_CFLAGS := -DHERMES_ENABLE_DEBUGGER=1
LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi

LOCAL_SRC_FILES := $(wildcard $(LOCAL_PATH)/*.cpp)
LOCAL_CPP_FEATURES := exceptions

LOCAL_C_INCLUDES := $(LOCAL_PATH) $(REACT_NATIVE)/ReactCommon/jsi $(call find-node-module,$(LOCAL_PATH),hermes-engine)/android/include
LOCAL_STATIC_LIBRARIES := libjsireact libhermes-executor-common-release
LOCAL_SHARED_LIBRARIES := \
libfb \
libfbjni \
libfolly_json \
libhermes \
libjsi \
libreactnativejni

LOCAL_CPP_FEATURES := exceptions

LOCAL_STATIC_LIBRARIES := libjsireact libhermes-executor-common-debug
LOCAL_SHARED_LIBRARIES := \
libfb \
libfbjni \
libfolly_json \
libhermes \
libjsi \
libreactnativejni

include $(BUILD_SHARED_LIBRARY)
include $(BUILD_SHARED_LIBRARY)
endif
10 changes: 0 additions & 10 deletions ReactAndroid/src/main/jni/first-party/hermes/Android.mk

This file was deleted.

9 changes: 8 additions & 1 deletion ReactAndroid/src/main/jni/react/jni/Android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -143,11 +143,18 @@ $(call import-module,jsiexecutor)
$(call import-module,logger)
$(call import-module,callinvoker)
$(call import-module,reactperflogger)
$(call import-module,hermes)
$(call import-module,runtimeexecutor)
$(call import-module,react/renderer/runtimescheduler)
$(call import-module,react/nativemodule/core)

# This block is needed only because we build the project on NDK r17 internally.
ifneq ($(call ndk-major-at-least,21),true)
$(call import-add-path,$(NDK_GRADLE_INJECTED_IMPORT_PATH))
endif

$(call import-module,prefab/hermes-engine)


include $(REACT_SRC_DIR)/reactperflogger/jni/Android.mk
# TODO (T48588859): Restructure this target to align with dir structure: "react/nativemodule/..."
# Note: Update this only when ready to minimize breaking changes.
Expand Down
29 changes: 0 additions & 29 deletions ReactCommon/common.mk

This file was deleted.

Loading

0 comments on commit a3d9892

Please sign in to comment.