import groovy.json.JsonSlurper import org.apache.tools.ant.filters.ReplaceTokens import java.nio.file.Paths static def findNodeModules(baseDir) { 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 nodeModulesPath = Paths.get(basePath.toString(), "node_modules") def reactNativePath = Paths.get(nodeModulesPath.toString(), "react-native") if (nodeModulesPath.toFile().exists() && reactNativePath.toFile().exists()) { return nodeModulesPath.toString() } basePath = basePath.getParent() } throw new GradleException("VisionCamera: Failed to find node_modules/ path!") } static 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 isNewArchitectureEnabled() { // To opt-in for the New Architecture, you can either: // - Set `newArchEnabled` to true inside the `gradle.properties` file // - Invoke gradle with `-newArchEnabled=true` // - Set an environment variable `ORG_GRADLE_PROJECT_newArchEnabled=true` return project.hasProperty("newArchEnabled") && project.newArchEnabled == "true" } def nodeModules = findNodeModules(projectDir) logger.warn("VisionCamera: node_modules/ found at: ${nodeModules}") def reactNative = new File("$nodeModules/react-native") def CMAKE_NODE_MODULES_DIR = project.getProjectDir().getParentFile().getParent() def reactProperties = new Properties() file("$nodeModules/react-native/ReactAndroid/gradle.properties").withInputStream { reactProperties.load(it) } def REACT_NATIVE_FULL_VERSION = reactProperties.getProperty("VERSION_NAME") def REACT_NATIVE_VERSION = reactProperties.getProperty("VERSION_NAME").split("\\.")[1].toInteger() def FOR_HERMES = System.getenv("FOR_HERMES") == "True" rootProject.getSubprojects().forEach({project -> if (project.plugins.hasPlugin("com.android.application")) { FOR_HERMES = project.ext.react.enableHermes } }) def jsRuntimeDir = { if (FOR_HERMES) { if (REACT_NATIVE_VERSION >= 69) { return Paths.get(CMAKE_NODE_MODULES_DIR, "react-native", "sdks", "hermes") } else { return Paths.get(CMAKE_NODE_MODULES_DIR, "hermes-engine") } } else { return Paths.get(CMAKE_NODE_MODULES_DIR, "react-native", "ReactCommon", "jsi") } }.call() logger.warn("VisionCamera: Building with ${FOR_HERMES ? "Hermes" : "JSC"}...") buildscript { // Buildscript is evaluated before everything else so we can't use getExtOrDefault def kotlin_version = rootProject.ext.has('kotlinVersion') ? rootProject.ext.get('kotlinVersion') : project.properties['VisionCamera_kotlinVersion'] repositories { google() mavenCentral() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath 'com.android.tools.build:gradle:4.2.2' classpath 'de.undercouch:gradle-download-task:4.1.2' // noinspection DifferentKotlinGradleVersion classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" } } apply plugin: 'com.android.library' apply plugin: 'de.undercouch.download' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' def getExtOrDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['VisionCamera_' + name] } def getExtOrIntegerDefault(name) { return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties['VisionCamera_' + name]).toInteger() } def resolveBuildType() { def buildType = "debug" tasks.all({ task -> if (task.name == "buildCMakeRelease") { buildType = "release" } }) return buildType } // plugin.js file only exists since REA v2. def hasReanimated2 = file("${nodeModules}/react-native-reanimated/plugin.js").exists() def disableFrameProcessors = rootProject.ext.has("disableFrameProcessors") ? rootProject.ext.get("disableFrameProcessors").asBoolean() : false def ENABLE_FRAME_PROCESSORS = hasReanimated2 && !disableFrameProcessors if (ENABLE_FRAME_PROCESSORS) { logger.warn("VisionCamera: Frame Processors are enabled! Building C++ part...") } else { if (disableFrameProcessors) { logger.warn("VisionCamera: Frame Processors are disabled because the user explicitly disabled it ('disableFrameProcessors=${disableFrameProcessors}'). C++ part will not be built.") } else if (!hasReanimated2) { logger.warn("VisionCamera: Frame Processors are disabled because REA v2 does not exist. C++ part will not be built.") } } android { compileSdkVersion getExtOrIntegerDefault('compileSdkVersion') buildToolsVersion getExtOrDefault('buildToolsVersion') ndkVersion getExtOrDefault('ndkVersion') defaultConfig { minSdkVersion 21 targetSdkVersion getExtOrIntegerDefault('targetSdkVersion') if (ENABLE_FRAME_PROCESSORS) { externalNativeBuild { cmake { cppFlags "-fexceptions", "-frtti", "-std=c++1y", "-DONANDROID" abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a' arguments '-DANDROID_STL=c++_shared', "-DREACT_NATIVE_VERSION=${REACT_NATIVE_VERSION}", "-DNODE_MODULES_DIR=${nodeModules}", "-DFOR_HERMES=${FOR_HERMES}", "-DJS_RUNTIME_DIR=${jsRuntimeDir}" } } } } dexOptions { javaMaxHeapSize "4g" } if (ENABLE_FRAME_PROCESSORS) { externalNativeBuild { cmake { path "CMakeLists.txt" } } } packagingOptions { // Exclude all Libraries that are already present in the user's app (through React Native or by him installing REA) excludes = ["**/libc++_shared.so", "**/libfbjni.so", "**/libjsi.so", "**/libreactnativejni.so", "**/libfolly_json.so", "**/libreanimated.so", "**/libjscexecutor.so", "**/libhermes.so"] // META-INF is duplicate by CameraX. exclude "META-INF/**" } buildTypes { release { minifyEnabled false } } lintOptions { disable 'GradleCompatible' } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } configurations { extractHeaders extractJNI } } repositories { mavenCentral() google() def found = false def defaultDir = null def androidSourcesName = 'React Native sources' if (rootProject.ext.has('reactNativeAndroidRoot')) { defaultDir = rootProject.ext.get('reactNativeAndroidRoot') } else { defaultDir = new File( projectDir, '/../../../node_modules/react-native/android' ) } if (defaultDir.exists()) { maven { url defaultDir.toString() name androidSourcesName } logger.info(":${project.name}:reactNativeAndroidRoot ${defaultDir.canonicalPath}") found = true } else { def parentDir = rootProject.projectDir 1.upto(5, { if (found) return true parentDir = parentDir.parentFile def androidSourcesDir = new File( parentDir, 'node_modules/react-native' ) def androidPrebuiltBinaryDir = new File( parentDir, 'node_modules/react-native/android' ) if (androidPrebuiltBinaryDir.exists()) { maven { url androidPrebuiltBinaryDir.toString() name androidSourcesName } logger.info(":${project.name}:reactNativeAndroidRoot ${androidPrebuiltBinaryDir.canonicalPath}") found = true } else if (androidSourcesDir.exists()) { maven { url androidSourcesDir.toString() name androidSourcesName } logger.info(":${project.name}:reactNativeAndroidRoot ${androidSourcesDir.canonicalPath}") found = true } }) } if (!found) { throw new GradleException( "${project.name}: unable to locate React Native android sources. " + "Ensure you have you installed React Native as a dependency in your project and try again." ) } } def kotlin_version = getExtOrDefault('kotlinVersion') dependencies { // noinspection GradleDynamicVersion implementation 'com.facebook.react:react-native:+' if (ENABLE_FRAME_PROCESSORS) { implementation project(':react-native-reanimated') //noinspection GradleDynamicVersion extractHeaders("com.facebook.fbjni:fbjni:+:headers") //noinspection GradleDynamicVersion extractJNI("com.facebook.fbjni:fbjni:+") def rnAarMatcher = "**/react-native/**/*${resolveBuildType()}.aar" if (REACT_NATIVE_VERSION < 69) { rnAarMatcher = "**/**/*.aar" } def rnAAR = fileTree("$reactNative/android").matching({ it.include rnAarMatcher }).singleFile def jscAAR = fileTree("${nodeModules}/jsc-android/dist/org/webkit/android-jsc").matching({ it.include "**/**/*.aar" }).singleFile def jsEngine = FOR_HERMES ? "hermes" : "jsc" def reaAAR = "${nodeModules}/react-native-reanimated/android/react-native-reanimated-${REACT_NATIVE_VERSION}-${jsEngine}.aar" extractJNI(files(rnAAR, jscAAR, reaAAR)) } implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.5.2" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.2" implementation "androidx.camera:camera-core:1.1.0-beta02" implementation "androidx.camera:camera-camera2:1.1.0-beta02" implementation "androidx.camera:camera-lifecycle:1.1.0-beta02" implementation "androidx.camera:camera-video:1.1.0-beta02" implementation "androidx.camera:camera-view:1.1.0-beta02" implementation "androidx.camera:camera-extensions:1.1.0-beta02" implementation "androidx.exifinterface:exifinterface:1.3.3" } if (ENABLE_FRAME_PROCESSORS) { // third-party-ndk deps headers // mostly a copy of https://github.com/software-mansion/react-native-reanimated/blob/master/android/build.gradle#L115 def downloadsDir = new File("$buildDir/downloads") def thirdPartyNdkDir = new File("$buildDir/third-party-ndk") def thirdPartyVersionsFile = new File("${nodeModules}/react-native/ReactAndroid/gradle.properties") def thirdPartyVersions = new Properties() thirdPartyVersions.load(new FileInputStream(thirdPartyVersionsFile)) def BOOST_VERSION = thirdPartyVersions["BOOST_VERSION"] def boost_file = new File(downloadsDir, "boost_${BOOST_VERSION}.tar.gz") def DOUBLE_CONVERSION_VERSION = thirdPartyVersions["DOUBLE_CONVERSION_VERSION"] def double_conversion_file = new File(downloadsDir, "double-conversion-${DOUBLE_CONVERSION_VERSION}.tar.gz") def FOLLY_VERSION = thirdPartyVersions["FOLLY_VERSION"] def folly_file = new File(downloadsDir, "folly-${FOLLY_VERSION}.tar.gz") def GLOG_VERSION = thirdPartyVersions["GLOG_VERSION"] def glog_file = new File(downloadsDir, "glog-${GLOG_VERSION}.tar.gz") task createNativeDepsDirectories { doLast { downloadsDir.mkdirs() thirdPartyNdkDir.mkdirs() } } task downloadBoost(dependsOn: createNativeDepsDirectories, type: Download) { def transformedVersion = BOOST_VERSION.replace("_", ".") def srcUrl = "https://boostorg.jfrog.io/artifactory/main/release/${transformedVersion}/source/boost_${BOOST_VERSION}.tar.gz" if (REACT_NATIVE_VERSION < 69) { srcUrl = "https://github.com/react-native-community/boost-for-react-native/releases/download/v${transformedVersion}-0/boost_${BOOST_VERSION}.tar.gz" } src(srcUrl) onlyIfNewer(true) overwrite(false) dest(boost_file) } task prepareBoost(dependsOn: downloadBoost, type: Copy) { from(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_X_XX_X doLast { file("$thirdPartyNdkDir/boost_${BOOST_VERSION}").renameTo("$thirdPartyNdkDir/boost") } } 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(double_conversion_file) } task prepareDoubleConversion(dependsOn: downloadDoubleConversion, type: Copy) { from(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(folly_file) } task prepareFolly(dependsOn: downloadFolly, type: Copy) { from(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 downloadGlog(dependsOn: createNativeDepsDirectories, type: Download) { src("https://github.com/google/glog/archive/v${GLOG_VERSION}.tar.gz") onlyIfNewer(true) overwrite(false) dest(glog_file) } task prepareGlog(dependsOn: downloadGlog, type: Copy) { from(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") } } } task prepareThirdPartyNdkHeaders { if (!boost_file.exists()) { dependsOn(prepareBoost) } if (!double_conversion_file.exists()) { dependsOn(prepareDoubleConversion) } if (!folly_file.exists()) { dependsOn(prepareFolly) } if (!glog_file.exists()) { dependsOn(prepareGlog) } } prepareThirdPartyNdkHeaders.mustRunAfter createNativeDepsDirectories /* COPY-PASTE from react-native-reanimated. Vision Camera includes "hermes/hermes.h" header file in `NativeProxy.cpp`. Previously, we used header files from `hermes-engine` package in `node_modules`. Starting from React Native 0.69, Hermes is no longer distributed as package on NPM. On the new architecture, Hermes is downloaded from GitHub and then compiled from sources. However, on the old architecture, we need to download Hermes header files on our own as well as unzip Hermes AAR in order to obtain `libhermes.so` shared library. For more details, see https://reactnative.dev/architecture/bundled-hermes or https://github.com/reactwg/react-native-new-architecture/discussions/4 */ if (REACT_NATIVE_VERSION >= 69 && !isNewArchitectureEnabled()) { // copied from `react-native/ReactAndroid/hermes-engine/build.gradle` def customDownloadDir = System.getenv("REACT_NATIVE_DOWNLOADS_DIR") def downloadDir = customDownloadDir ? new File(customDownloadDir) : new File(reactNative, "sdks/download") // By default we are going to download and unzip hermes inside the /sdks/hermes folder // but you can provide an override for where the hermes source code is located. def hermesDir = System.getenv("REACT_NATIVE_OVERRIDE_HERMES_DIR") ?: new File(reactNative, "sdks/hermes") def hermesVersion = "main" def hermesVersionFile = new File(reactNative, "sdks/.hermesversion") if (hermesVersionFile.exists()) { hermesVersion = hermesVersionFile.text } task downloadHermes(type: Download) { src("https://github.com/facebook/hermes/tarball/${hermesVersion}") onlyIfNewer(true) overwrite(false) dest(new File(downloadDir, "hermes.tar.gz")) } task unzipHermes(dependsOn: downloadHermes, type: Copy) { from(tarTree(downloadHermes.dest)) { eachFile { file -> // We flatten the unzip as the tarball contains a `facebook-hermes-` // folder at the top level. if (file.relativePath.segments.size() > 1) { file.relativePath = new org.gradle.api.file.RelativePath(!file.isDirectory(), file.relativePath.segments.drop(1)) } } } into(hermesDir) } } task prepareHermes() { if (REACT_NATIVE_VERSION >= 69) { if (!isNewArchitectureEnabled()) { dependsOn(unzipHermes) } doLast { def hermesAAR = file("$reactNative/android/com/facebook/react/hermes-engine/$REACT_NATIVE_FULL_VERSION/hermes-engine-$REACT_NATIVE_FULL_VERSION-${resolveBuildType()}.aar") // e.g. hermes-engine-0.70.0-rc.1-debug.aar if (!hermesAAR.exists()) { throw new GradleScriptException("Could not find hermes-engine AAR", null) } def soFiles = zipTree(hermesAAR).matching({ it.include "**/*.so" }) copy { from soFiles from "$reactNative/ReactAndroid/src/main/jni/first-party/hermes/Android.mk" into "$thirdPartyNdkDir/hermes" } } } else { doLast { 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" }) copy { from soFiles from "$reactNative/ReactAndroid/src/main/jni/first-party/hermes/Android.mk" into "$thirdPartyNdkDir/hermes" } } } } prepareHermes.mustRunAfter prepareThirdPartyNdkHeaders task prepareJSC { doLast { def jscPackagePath = file("${nodeModules}/jsc-android") if (!jscPackagePath.exists()) { 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("$reactNative/ReactAndroid/src/main/jni/third-party/jsc/Android.mk") filesMatching("**/*.h", { it.path = "JavaScriptCore/${it.name}" }) includeEmptyDirs(false) into("$thirdPartyNdkDir/jsc") } } } prepareJSC.mustRunAfter prepareHermes task extractAARHeaders { doLast { configurations.extractHeaders.files.each { def file = it.absoluteFile copy { from zipTree(file) into "$buildDir/$file.name" include "**/*.h" } } } } extractAARHeaders.mustRunAfter prepareJSC task extractJNIFiles { doLast { configurations.extractJNI.files.each { def file = it.absoluteFile copy { from zipTree(file) into "$buildDir/$file.name" include "jni/**/*" } } } } extractJNIFiles.mustRunAfter extractAARHeaders // pre-native build pipeline tasks.whenTaskAdded { task -> if (!task.name.contains('Clean') && (task.name.contains('externalNative') || task.name.contains('CMake'))) { task.dependsOn(extractAARHeaders) task.dependsOn(extractJNIFiles) task.dependsOn(prepareJSC) task.dependsOn(prepareHermes) task.dependsOn(prepareThirdPartyNdkHeaders) } } }