Files
Andrei Shikov 121a6a49c6 Fix Android build sequencing
Summary:
The native libraries are compiled outside of the usual Android build flow using separate CLI task. Because of that, shared native libraries may not exist when AAR is bundled, resulting in weird sequencing issues.

This change updates gradle dependency graph, executing RN native build before Android part (as it is done in RNTester already).

Changelog: [Internal]

Reviewed By: mdvacca

Differential Revision: D29209249

fbshipit-source-id: 36386c78996b1cd9b1731735e36e571199e9e81b
2021-06-18 09:22:08 +01:00

530 lines
20 KiB
Groovy

/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
plugins {
id("com.android.library")
id("com.facebook.react.codegen")
id("maven-publish")
id("de.undercouch.download")
}
import java.nio.file.Paths
import de.undercouch.gradle.tasks.download.Download
import org.apache.tools.ant.taskdefs.condition.Os
import org.apache.tools.ant.filters.ReplaceTokens
def AAR_OUTPUT_URL = "file://${projectDir}/../android"
// We download various C++ open-source dependencies into downloads.
// We then copy both the downloaded code and our custom makefiles and headers into third-party-ndk.
// After that we build native code from src/main/jni with module path pointing at third-party-ndk.
def customDownloadsDir = System.getenv("REACT_NATIVE_DOWNLOADS_DIR")
def downloadsDir = customDownloadsDir ? new File(customDownloadsDir) : new File("$buildDir/downloads")
def thirdPartyNdkDir = new File("$buildDir/third-party-ndk")
// You need to have following folders in this directory:
// - boost_1_63_0
// - double-conversion-1.1.6
// - folly-deprecate-dynamic-initializer
// - glog-0.3.5
def dependenciesPath = System.getenv("REACT_NATIVE_DEPENDENCIES")
// The Boost library is a very large download (>100MB).
// If Boost is already present on your system, define the REACT_NATIVE_BOOST_PATH env variable
// and the build will use that.
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"
task createNativeDepsDirectories {
downloadsDir.mkdirs()
thirdPartyNdkDir.mkdirs()
}
task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) {
src("https://github.com/react-native-community/boost-for-react-native/releases/download/v${BOOST_VERSION.replace("_", ".")}-0/boost_${BOOST_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz"))
}
task prepareBoost(dependsOn: boostPath ? [] : [downloadBoost], type: Copy) {
from(boostPath ?: tarTree(resources.gzip(downloadBoost.dest)))
from("src/main/jni/third-party/boost/Android.mk")
include("Android.mk", "boost_${BOOST_VERSION}/boost/**/*.hpp", "boost/boost/**/*.hpp")
includeEmptyDirs = false
into("$thirdPartyNdkDir/boost")
doLast {
file("$thirdPartyNdkDir/boost/boost").renameTo("$thirdPartyNdkDir/boost/boost_${BOOST_VERSION}")
}
}
task downloadDoubleConversion(dependsOn: createNativeDepsDirectories, type: Download) {
src("https://github.com/google/double-conversion/archive/v${DOUBLE_CONVERSION_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz"))
}
task prepareDoubleConversion(dependsOn: dependenciesPath ? [] : [downloadDoubleConversion], type: Copy) {
from(dependenciesPath ?: tarTree(downloadDoubleConversion.dest))
from("src/main/jni/third-party/double-conversion/Android.mk")
include("double-conversion-${DOUBLE_CONVERSION_VERSION}/src/**/*", "Android.mk")
filesMatching("*/src/**/*", { fname -> fname.path = "double-conversion/${fname.name}" })
includeEmptyDirs = false
into("$thirdPartyNdkDir/double-conversion")
}
task downloadFolly(dependsOn: createNativeDepsDirectories, type: Download) {
src("https://github.com/facebook/folly/archive/v${FOLLY_VERSION}.tar.gz")
onlyIfNewer(true)
overwrite(false)
dest(new File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz"))
}
task prepareFolly(dependsOn: dependenciesPath ? [] : [downloadFolly], type: Copy) {
from(dependenciesPath ?: tarTree(downloadFolly.dest))
from("src/main/jni/third-party/folly/Android.mk")
include("folly-${FOLLY_VERSION}/folly/**/*", "Android.mk")
eachFile { fname -> fname.path = (fname.path - "folly-${FOLLY_VERSION}/") }
includeEmptyDirs = false
into("$thirdPartyNdkDir/folly")
}
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)
overwrite(false)
dest(new File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz"))
}
// Prepare glog sources to be compiled, this task will perform steps that normally should've been
// executed by automake. This way we can avoid dependencies on make/automake
task prepareGlog(dependsOn: dependenciesPath ? [] : [downloadGlog], type: Copy) {
from(dependenciesPath ?: tarTree(downloadGlog.dest))
from("src/main/jni/third-party/glog/")
include("glog-${GLOG_VERSION}/src/**/*", "Android.mk", "config.h")
includeEmptyDirs = false
filesMatching("**/*.h.in") {
filter(ReplaceTokens, tokens: [
ac_cv_have_unistd_h : "1",
ac_cv_have_stdint_h : "1",
ac_cv_have_systypes_h : "1",
ac_cv_have_inttypes_h : "1",
ac_cv_have_libgflags : "0",
ac_google_start_namespace : "namespace google {",
ac_cv_have_uint16_t : "1",
ac_cv_have_u_int16_t : "1",
ac_cv_have___uint16 : "0",
ac_google_end_namespace : "}",
ac_cv_have___builtin_expect : "1",
ac_google_namespace : "google",
ac_cv___attribute___noinline : "__attribute__ ((noinline))",
ac_cv___attribute___noreturn : "__attribute__ ((noreturn))",
ac_cv___attribute___printf_4_5: "__attribute__((__format__ (__printf__, 4, 5)))"
])
it.path = (it.name - ".in")
}
into("$thirdPartyNdkDir/glog")
doLast {
copy {
from(fileTree(dir: "$thirdPartyNdkDir/glog", includes: ["stl_logging.h", "logging.h", "raw_logging.h", "vlog_is_on.h", "**/src/glog/log_severity.h"]).files)
includeEmptyDirs = false
into("$thirdPartyNdkDir/glog/exported/glog")
}
}
}
// Create Android.mk library module based on jsc from npm
task prepareJSC {
doLast {
def jscPackagePath = findNodeModulePath(projectDir, "jsc-android")
if (!jscPackagePath) {
throw new GradleScriptException("Could not find the jsc-android npm package", null)
}
def jscDist = file("$jscPackagePath/dist")
if (!jscDist.exists()) {
throw new GradleScriptException("The jsc-android npm package is missing its \"dist\" directory", null)
}
def jscAAR = fileTree(jscDist).matching({ it.include "**/android-jsc/**/*.aar" }).singleFile
def soFiles = zipTree(jscAAR).matching({ it.include "**/*.so" })
def headerFiles = fileTree(jscDist).matching({ it.include "**/include/*.h" })
copy {
from(soFiles)
from(headerFiles)
from("src/main/jni/third-party/jsc/Android.mk")
filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" })
includeEmptyDirs(false)
into("$thirdPartyNdkDir/jsc")
}
}
}
task downloadNdkBuildDependencies {
if (!boostPath) {
dependsOn(downloadBoost)
}
dependsOn(downloadDoubleConversion)
dependsOn(downloadFolly)
dependsOn(downloadGlog)
}
/**
* Finds the path of the installed npm package with the given name using Node's
* module resolution algorithm, which searches "node_modules" directories up to
* the file system root. This handles various cases, including:
*
* - Working in the open-source RN repo:
* Gradle: /path/to/react-native/ReactAndroid
* Node module: /path/to/react-native/node_modules/[package]
*
* - Installing RN as a dependency of an app and searching for hoisted
* dependencies:
* Gradle: /path/to/app/node_modules/react-native/ReactAndroid
* Node module: /path/to/app/node_modules/[package]
*
* - Working in a larger repo (e.g., Facebook) that contains RN:
* Gradle: /path/to/repo/path/to/react-native/ReactAndroid
* Node module: /path/to/repo/node_modules/[package]
*
* The search begins at the given base directory (a File object). The returned
* path is a string.
*/
def findNodeModulePath(baseDir, packageName) {
def basePath = baseDir.toPath().normalize()
// Node's module resolution algorithm searches up to the root directory,
// after which the base path will be null
while (basePath) {
def candidatePath = Paths.get(basePath.toString(), "node_modules", packageName)
if (candidatePath.toFile().exists()) {
return candidatePath.toString()
}
basePath = basePath.getParent()
}
return null
}
def getNdkBuildName() {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
return "ndk-build.cmd"
} else {
return "ndk-build"
}
}
def findNdkBuildFullPath() {
// android.ndkDirectory should return project.android.ndkVersion ndkDirectory
def ndkDir = android.ndkDirectory ? android.ndkDirectory.absolutePath : null
if (ndkDir) {
return new File(ndkDir, getNdkBuildName()).getAbsolutePath()
}
// we allow to provide full path to ndk-build tool
if (hasProperty("ndk.command")) {
return property("ndk.command")
}
// or just a path to the containing directory
if (hasProperty("ndk.path")) {
ndkDir = property("ndk.path")
return new File(ndkDir, getNdkBuildName()).getAbsolutePath()
}
// @TODO ANDROID_NDK && ndk.dir is deprecated and will be removed in the future.
if (System.getenv("ANDROID_NDK") != null) {
ndkDir = System.getenv("ANDROID_NDK")
return new File(ndkDir, getNdkBuildName()).getAbsolutePath()
}
return null
}
def reactNativeDevServerPort() {
def value = project.getProperties().get("reactNativeDevServerPort")
return value != null ? value : "8081"
}
def reactNativeInspectorProxyPort() {
def value = project.getProperties().get("reactNativeInspectorProxyPort")
return value != null ? value : reactNativeDevServerPort()
}
def getNdkBuildFullPath() {
def ndkBuildFullPath = findNdkBuildFullPath()
if (ndkBuildFullPath == null) {
throw new GradleScriptException(
"ndk-build binary cannot be found, check if you've set " +
"\$ANDROID_NDK environment variable correctly or if ndk.dir is " +
"setup in local.properties",
null)
}
if (!new File(ndkBuildFullPath).canExecute()) {
throw new GradleScriptException(
"ndk-build binary " + ndkBuildFullPath + " doesn't exist or isn't executable.\n" +
"Check that the \$ANDROID_NDK environment variable, or ndk.dir in local.properties, is set correctly.\n" +
"(On Windows, make sure you escape backslashes in local.properties or use forward slashes, e.g. C:\\\\ndk or C:/ndk rather than C:\\ndk)",
null)
}
return ndkBuildFullPath
}
def buildReactNdkLib = tasks.register("buildReactNdkLib", Exec) {
dependsOn(prepareJSC, prepareHermes, prepareBoost, prepareDoubleConversion, prepareFolly, prepareGlog, extractAARHeaders, extractJNIFiles)
dependsOn("generateCodegenArtifactsFromSchema");
inputs.dir("$projectDir/../ReactCommon")
inputs.dir("src/main/jni")
inputs.dir("src/main/java/com/facebook/react/turbomodule/core/jni")
inputs.dir("src/main/java/com/facebook/react/modules/blob")
outputs.dir("$buildDir/react-ndk/all")
commandLine(getNdkBuildFullPath(),
"NDK_DEBUG=" + (nativeBuildType.equalsIgnoreCase("debug") ? "1" : "0"),
"NDK_PROJECT_PATH=null",
"NDK_APPLICATION_MK=$projectDir/src/main/jni/Application.mk",
"NDK_OUT=" + temporaryDir,
"NDK_LIBS_OUT=$buildDir/react-ndk/all",
"THIRD_PARTY_NDK_DIR=$thirdPartyNdkDir",
"REACT_COMMON_DIR=$projectDir/../ReactCommon",
"REACT_GENERATED_SRC_DIR=$buildDir/generated/source",
"REACT_SRC_DIR=$projectDir/src/main/java/com/facebook/react",
"-C", file("src/main/jni/react/jni").absolutePath,
"--jobs", project.findProperty("jobs") ?: Runtime.runtime.availableProcessors()
)
}
def cleanReactNdkLib = tasks.register("cleanReactNdkLib", Exec) {
ignoreExitValue(true)
errorOutput(new ByteArrayOutputStream())
commandLine(getNdkBuildFullPath(),
"NDK_APPLICATION_MK=$projectDir/src/main/jni/Application.mk",
"THIRD_PARTY_NDK_DIR=$thirdPartyNdkDir",
"REACT_COMMON_DIR=$projectDir/../ReactCommon",
"-C", file("src/main/jni/react/jni").absolutePath,
"clean")
doLast {
file(AAR_OUTPUT_URL).delete()
println("Deleted aar output dir at ${file(AAR_OUTPUT_URL)}")
}
}
def packageReactNdkLibs = tasks.register("packageReactNdkLibs", Copy) {
dependsOn(buildReactNdkLib)
from("$buildDir/react-ndk/all")
into("$buildDir/react-ndk/exported")
exclude("**/libjsc.so")
exclude("**/libhermes.so")
}
def packageReactNdkLibsForBuck = tasks.register("packageReactNdkLibsForBuck", Copy) {
dependsOn(packageReactNdkLibs)
from("$buildDir/react-ndk/exported")
into("src/main/jni/prebuilt/lib")
}
task extractAARHeaders {
doLast {
configurations.extractHeaders.files.each {
def file = it.absoluteFile
def packageName = file.name.tokenize('-')[0]
copy {
from zipTree(file)
into "$projectDir/src/main/jni/first-party/$packageName/headers"
include "**/*.h"
}
}
}
}
task extractJNIFiles {
doLast {
configurations.extractJNI.files.each {
def file = it.absoluteFile
def packageName = file.name.tokenize('-')[0]
copy {
from zipTree(file)
into "$projectDir/src/main/jni/first-party/$packageName/"
include "jni/**/*"
}
}
}
}
task installArchives {
dependsOn("publishReleasePublicationToNpmRepository")
}
android {
compileSdkVersion 30
ndkVersion ANDROID_NDK_VERSION
defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
versionCode(1)
versionName("1.0")
consumerProguardFiles("proguard-rules.pro")
buildConfigField("boolean", "IS_INTERNAL_BUILD", "false")
buildConfigField("int", "EXOPACKAGE_FLAGS", "0")
buildConfigField("int", "HERMES_BYTECODE_VERSION", "0")
resValue "integer", "react_native_dev_server_port", reactNativeDevServerPort()
resValue "integer", "react_native_inspector_proxy_port", reactNativeInspectorProxyPort()
testApplicationId("com.facebook.react.tests.gradle")
testInstrumentationRunner("androidx.test.runner.AndroidJUnitRunner")
}
sourceSets.main {
jni.srcDirs = []
jniLibs.srcDir("$buildDir/react-ndk/exported")
res.srcDirs = ["src/main/res/devsupport", "src/main/res/shell", "src/main/res/views/modal", "src/main/res/views/uimanager"]
java {
srcDirs = ["src/main/java", "src/main/libraries/soloader/java", "src/main/jni/first-party/fb/jni/java"]
exclude("com/facebook/react/processing")
exclude("com/facebook/react/module/processing")
}
}
preBuild.dependsOn(packageReactNdkLibs)
clean.dependsOn(cleanReactNdkLib)
lintOptions {
abortOnError(false)
}
packagingOptions {
exclude("META-INF/NOTICE")
exclude("META-INF/LICENSE")
}
configurations {
extractHeaders
extractJNI
javadocDeps.extendsFrom api
}
}
dependencies {
api("com.facebook.infer.annotation:infer-annotation:0.11.2")
api("com.facebook.yoga:proguard-annotations:1.19.0")
api("javax.inject:javax.inject:1")
api("androidx.appcompat:appcompat:1.0.2")
api("androidx.swiperefreshlayout:swiperefreshlayout:1.0.0")
api("com.facebook.fresco:fresco:${FRESCO_VERSION}")
api("com.facebook.fresco:imagepipeline-okhttp3:${FRESCO_VERSION}")
api("com.facebook.fresco:ui-common:${FRESCO_VERSION}")
api("com.facebook.soloader:soloader:${SO_LOADER_VERSION}")
api("com.google.code.findbugs:jsr305:3.0.2")
api("com.squareup.okhttp3:okhttp:${OKHTTP_VERSION}")
api("com.squareup.okhttp3:okhttp-urlconnection:${OKHTTP_VERSION}")
api("com.squareup.okio:okio:2.9.0")
api("com.facebook.fbjni:fbjni-java-only:0.2.2")
extractHeaders("com.facebook.fbjni:fbjni:0.2.2:headers")
extractJNI("com.facebook.fbjni:fbjni:0.2.2")
javadocDeps("com.squareup:javapoet:1.13.0")
testImplementation("junit:junit:${JUNIT_VERSION}")
testImplementation("org.powermock:powermock-api-mockito2:${POWERMOCK_VERSION}")
testImplementation("org.powermock:powermock-module-junit4-rule:${POWERMOCK_VERSION}")
testImplementation("org.powermock:powermock-classloading-xstream:${POWERMOCK_VERSION}")
testImplementation("org.mockito:mockito-core:${MOCKITO_CORE_VERSION}")
testImplementation("org.easytesting:fest-assert-core:2.0M10")
testImplementation("org.robolectric:robolectric:${ROBOLECTRIC_VERSION}")
androidTestImplementation(fileTree(dir: "src/main/third-party/java/buck-android-support/", include: ["*.jar"]))
androidTestImplementation("androidx.test:runner:${ANDROIDX_TEST_VERSION}")
androidTestImplementation("androidx.test:rules:${ANDROIDX_TEST_VERSION}")
androidTestImplementation("org.mockito:mockito-core:${MOCKITO_CORE_VERSION}")
}
react {
// TODO: The library name is chosen for parity with Fabric components & iOS
// This should be changed to a more generic name, e.g. `ReactCoreSpec`.
libraryName = "rncore"
jsRootDir = file("../Libraries")
reactNativeRootDir = file("$projectDir/..")
useJavaGenerator = System.getenv("USE_CODEGEN_JAVAPOET") ?: false
}
afterEvaluate {
publishing {
publications {
release(MavenPublication) {
// Applies the component for the release build variant.
from components.release
// You can then customize attributes of the publication as shown below.
artifactId = POM_ARTIFACT_ID
groupId = GROUP
version = VERSION_NAME
pom {
name = POM_NAME
description = "A framework for building native apps with React"
url = "https://github.com/facebook/react-native"
developers {
developer {
id = "facebook"
name = "Facebook"
}
}
licenses {
license {
name = "MIT License"
url = "https://github.com/facebook/react-native/blob/master/LICENSE"
distribution = "repo"
}
}
scm {
url = "https://github.com/facebook/react-native.git"
connection = "scm:git:https://github.com/facebook/react-native.git"
developerConnection = "scm:git:git@github.com:facebook/react-native.git"
}
}
}
}
repositories {
maven {
name = "npm"
url = AAR_OUTPUT_URL
}
}
}
}