diff --git a/.gitignore b/.gitignore index d481899882..5c7b0308a0 100644 --- a/.gitignore +++ b/.gitignore @@ -98,6 +98,15 @@ docs/docs/06-api-reference/ # integration test model assets packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/models/ +# release artifact staging dir (produced by scripts/package-release-artifacts.sh) +packages/react-native-executorch/dist-artifacts/ + +# on-demand native libs (downloaded at postinstall time, not committed) +packages/react-native-executorch/third-party/android/libs/ +packages/react-native-executorch/third-party/ios/ExecutorchLib.xcframework/ +packages/react-native-executorch/third-party/ios/libs/ +packages/react-native-executorch/rne-build-config.json + # custom *.tgz Makefile diff --git a/packages/react-native-executorch/android/CMakeLists.txt b/packages/react-native-executorch/android/CMakeLists.txt index ddc7ab4126..29cbc092ee 100644 --- a/packages/react-native-executorch/android/CMakeLists.txt +++ b/packages/react-native-executorch/android/CMakeLists.txt @@ -24,6 +24,10 @@ set(LIBS_DIR "${CMAKE_SOURCE_DIR}/../third-party/android/libs") set(TOKENIZERS_DIR "${CMAKE_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/include") set(INCLUDE_DIR "${CMAKE_SOURCE_DIR}/../third-party/include") +# Optional feature flags — driven by user config in package.json, passed via gradle cmake arguments +option(RNE_ENABLE_OPENCV "Enable OpenCV-dependent computer vision features" ON) +option(RNE_ENABLE_PHONEMIZER "Enable Phonemizer-dependent TTS features" ON) + # Treat third-party headers as system headers to suppress deprecation warnings include_directories(SYSTEM "${INCLUDE_DIR}") diff --git a/packages/react-native-executorch/android/build.gradle b/packages/react-native-executorch/android/build.gradle index 5b1cfd2973..7812bc521c 100644 --- a/packages/react-native-executorch/android/build.gradle +++ b/packages/react-native-executorch/android/build.gradle @@ -1,5 +1,22 @@ import org.apache.tools.ant.taskdefs.condition.Os +// Read the generated build config written by the postinstall script. +// Falls back to enabling everything if the file doesn't exist (e.g. during CI +// when libs are pre-cached and the postinstall script skipped writing config). +def getRneBuildConfig() { + def configFile = new File("${project.projectDir}/../rne-build-config.json") + if (configFile.exists()) { + try { + return new groovy.json.JsonSlurper().parse(configFile) + } catch (e) { + logger.warn("[RnExecutorch] Failed to parse rne-build-config.json: ${e.message}. Defaulting to all features enabled.") + } + } + return [enableOpencv: true, enablePhonemizer: true] +} + +def rneBuildConfig = getRneBuildConfig() + buildscript { ext { agp_version = '8.4.2' @@ -122,7 +139,9 @@ android { "-DREACT_NATIVE_DIR=${toPlatformFileString(reactNativeRootDir.path)}", "-DBUILD_DIR=${project.buildDir}", "-DANDROID_TOOLCHAIN=clang", - "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON" + "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON", + "-DRNE_ENABLE_OPENCV=${rneBuildConfig.enableOpencv ? 'ON' : 'OFF'}", + "-DRNE_ENABLE_PHONEMIZER=${rneBuildConfig.enablePhonemizer ? 'ON' : 'OFF'}" } } } diff --git a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt index d7bd1fa870..48128f27a9 100644 --- a/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt +++ b/packages/react-native-executorch/android/src/main/cpp/CMakeLists.txt @@ -1,12 +1,74 @@ cmake_minimum_required(VERSION 3.13) file(GLOB_RECURSE ANDROID_CPP_SOURCES CONFIGURE_DEPENDS "${ANDROID_CPP_DIR}/*.cpp") -file(GLOB_RECURSE COMMON_CPP_SOURCES CONFIGURE_DEPENDS "${COMMON_CPP_DIR}/*.cpp") -file(GLOB_RECURSE COMMON_C_SOURCES CONFIGURE_DEPENDS "${COMMON_CPP_DIR}/*.c") + +# --- Source separation --- +# Glob all common sources, then separate opencv-dependent and phonemizer-dependent +# files so they can be conditionally included based on feature flags. + +file(GLOB_RECURSE ALL_COMMON_CPP_SOURCES CONFIGURE_DEPENDS "${COMMON_CPP_DIR}/*.cpp") +file(GLOB_RECURSE ALL_COMMON_C_SOURCES CONFIGURE_DEPENDS "${COMMON_CPP_DIR}/*.c") + +# Exclude test sources unconditionally file(GLOB_RECURSE TEST_CPP_SOURCES "${COMMON_CPP_DIR}/rnexecutorch/tests/*.cpp") -list(REMOVE_ITEM COMMON_CPP_SOURCES ${TEST_CPP_SOURCES}) +list(REMOVE_ITEM ALL_COMMON_CPP_SOURCES ${TEST_CPP_SOURCES}) + +# OpenCV-dependent sources: CV models + frame utilities + image processing +file(GLOB_RECURSE OPENCV_CPP_SOURCES CONFIGURE_DEPENDS + "${COMMON_CPP_DIR}/rnexecutorch/models/classification/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/object_detection/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/semantic_segmentation/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/instance_segmentation/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/style_transfer/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/ocr/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/vertical_ocr/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/embeddings/image/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/text_to_image/*.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/models/VisionModel.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/data_processing/ImageProcessing.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/utils/FrameExtractor.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/utils/FrameProcessor.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/utils/FrameTransform.cpp" + "${COMMON_CPP_DIR}/rnexecutorch/utils/computer_vision/*.cpp" + "${COMMON_CPP_DIR}/runner/encoders/vision_encoder.cpp" + "${COMMON_CPP_DIR}/runner/multimodal_prefiller.cpp" + "${COMMON_CPP_DIR}/runner/multimodal_runner.cpp" +) + +# Phonemizer-dependent sources: Kokoro TTS (only user of phonemis) +file(GLOB_RECURSE PHONEMIZER_CPP_SOURCES CONFIGURE_DEPENDS + "${COMMON_CPP_DIR}/rnexecutorch/models/text_to_speech/*.cpp" +) + +# Core = everything minus optional sources +set(CORE_COMMON_CPP_SOURCES ${ALL_COMMON_CPP_SOURCES}) +list(REMOVE_ITEM CORE_COMMON_CPP_SOURCES ${OPENCV_CPP_SOURCES} ${PHONEMIZER_CPP_SOURCES}) + +# Build final source list +set(ENABLED_COMMON_SOURCES ${CORE_COMMON_CPP_SOURCES}) -add_library(react-native-executorch SHARED ${ANDROID_CPP_SOURCES} ${COMMON_CPP_SOURCES} ${COMMON_C_SOURCES}) +if(RNE_ENABLE_OPENCV) + list(APPEND ENABLED_COMMON_SOURCES ${OPENCV_CPP_SOURCES}) +endif() + +if(RNE_ENABLE_PHONEMIZER) + list(APPEND ENABLED_COMMON_SOURCES ${PHONEMIZER_CPP_SOURCES}) +endif() + +add_library(react-native-executorch SHARED + ${ANDROID_CPP_SOURCES} + ${ENABLED_COMMON_SOURCES} + ${ALL_COMMON_C_SOURCES} +) + +# Propagate feature flags as preprocessor defines so C++ code can guard includes +if(RNE_ENABLE_OPENCV) + target_compile_definitions(react-native-executorch PRIVATE RNE_ENABLE_OPENCV) +endif() + +if(RNE_ENABLE_PHONEMIZER) + target_compile_definitions(react-native-executorch PRIVATE RNE_ENABLE_PHONEMIZER) +endif() find_package(ReactAndroid REQUIRED CONFIG) find_package(fbjni REQUIRED CONFIG) @@ -34,63 +96,55 @@ set(RN_VERSION_LINK_LIBRARIES ReactAndroid::reactnative ) -# Dependencies: - -# ------- Executorch ------- +# ------- Executorch (always required) ------- add_library(executorch SHARED IMPORTED) set_target_properties(executorch PROPERTIES IMPORTED_LOCATION "${LIBS_DIR}/executorch/${ANDROID_ABI}/libexecutorch.so") - if(ANDROID_ABI STREQUAL "arm64-v8a") target_compile_definitions(react-native-executorch PRIVATE ARCH_ARM64) - # ------- pthreadpool ------- add_library(pthreadpool SHARED IMPORTED) - set_target_properties(pthreadpool PROPERTIES IMPORTED_LOCATION "${LIBS_DIR}/pthreadpool/${ANDROID_ABI}/libpthreadpool.so") - # ------- cpuinfo ------- add_library(cpuinfo SHARED IMPORTED) - set_target_properties(cpuinfo PROPERTIES IMPORTED_LOCATION "${LIBS_DIR}/cpuinfo/${ANDROID_ABI}/libcpuinfo.so") - set(EXECUTORCH_LIBS - "pthreadpool" - "cpuinfo" - ) + + set(EXECUTORCH_LIBS "pthreadpool" "cpuinfo") endif() -# ------- OpenCV ------- +# ------- OpenCV (optional) ------- -set(OPENCV_LIBS - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_core.a" - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_features2d.a" - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_highgui.a" - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_imgproc.a" - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_photo.a" - "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_video.a" -) - -if(ANDROID_ABI STREQUAL "arm64-v8a") - set(OPENCV_THIRD_PARTY_LIBS - "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv_hal.a" - "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv_thread.a" - "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv.a" +if(RNE_ENABLE_OPENCV) + set(OPENCV_LINK_LIBS + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_core.a" + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_features2d.a" + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_highgui.a" + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_imgproc.a" + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_photo.a" + "${LIBS_DIR}/opencv/${ANDROID_ABI}/libopencv_video.a" ) -elseif(ANDROID_ABI STREQUAL "x86_64") - set(OPENCV_THIRD_PARTY_LIBS "") -endif() + if(ANDROID_ABI STREQUAL "arm64-v8a") + list(APPEND OPENCV_LINK_LIBS + "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv_hal.a" + "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv_thread.a" + "${LIBS_DIR}/opencv-third-party/${ANDROID_ABI}/libkleidicv.a" + ) + endif() +endif() -# ------- phonemis ------- +# ------- Phonemizer (optional) ------- -set(PHONEMIS_LIBS - "${LIBS_DIR}/phonemis/${ANDROID_ABI}/libphonemis.a" -) +if(RNE_ENABLE_PHONEMIZER) + set(PHONEMIZER_LINK_LIBS + "${LIBS_DIR}/phonemis/${ANDROID_ABI}/libphonemis.a" + ) +endif() # -------------- @@ -100,9 +154,8 @@ target_link_libraries( react-native-executorch ${LINK_LIBRARIES} ${RN_VERSION_LINK_LIBRARIES} - ${OPENCV_LIBS} - ${OPENCV_THIRD_PARTY_LIBS} - ${PHONEMIS_LIBS} + ${OPENCV_LINK_LIBS} + ${PHONEMIZER_LINK_LIBS} executorch ${EXECUTORCH_LIBS} z diff --git a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp index da75ab951c..294d0080ba 100644 --- a/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/RnExecutorchInstaller.cpp @@ -2,22 +2,28 @@ #include #include +#include +#include +#include +#include +#include +#include + +#ifdef RNE_ENABLE_OPENCV #include #include -#include #include -#include #include #include #include -#include #include #include -#include #include -#include -#include -#include +#endif + +#ifdef RNE_ENABLE_PHONEMIZER +#include +#endif #if defined(__ANDROID__) && defined(__aarch64__) #include @@ -40,6 +46,7 @@ void RnExecutorchInstaller::injectJSIBindings( jsiRuntime->global().setProperty(*jsiRuntime, "__rne_isEmulator", jsi::Value(isEmulator)); +#ifdef RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadStyleTransfer", RnExecutorchInstaller::loadModel( @@ -72,6 +79,7 @@ void RnExecutorchInstaller::injectJSIBindings( RnExecutorchInstaller::loadModel< models::object_detection::ObjectDetection>(jsiRuntime, jsCallInvoker, "loadObjectDetection")); +#endif // RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadExecutorchModule", @@ -83,10 +91,12 @@ void RnExecutorchInstaller::injectJSIBindings( RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadTokenizerModule")); +#ifdef RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadImageEmbeddings", RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadImageEmbeddings")); +#endif // RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadTextEmbeddings", @@ -98,6 +108,7 @@ void RnExecutorchInstaller::injectJSIBindings( RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadLLM")); +#ifdef RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadOCR", RnExecutorchInstaller::loadModel( @@ -107,16 +118,19 @@ void RnExecutorchInstaller::injectJSIBindings( *jsiRuntime, "loadVerticalOCR", RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadVerticalOCR")); +#endif // RNE_ENABLE_OPENCV jsiRuntime->global().setProperty( *jsiRuntime, "loadSpeechToText", RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadSpeechToText")); +#ifdef RNE_ENABLE_PHONEMIZER jsiRuntime->global().setProperty( *jsiRuntime, "loadTextToSpeechKokoro", RnExecutorchInstaller::loadModel( jsiRuntime, jsCallInvoker, "loadTextToSpeechKokoro")); +#endif // RNE_ENABLE_PHONEMIZER jsiRuntime->global().setProperty( *jsiRuntime, "loadVAD", diff --git a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp index 64e94c2ff0..642201d799 100644 --- a/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp +++ b/packages/react-native-executorch/common/rnexecutorch/models/llm/LLM.cpp @@ -6,9 +6,11 @@ #include #include #include +#include +#ifdef RNE_ENABLE_OPENCV #include #include -#include +#endif namespace rnexecutorch::models::llm { namespace llm = ::executorch::extension::llm; @@ -22,10 +24,8 @@ LLM::LLM(const std::string &modelSource, const std::string &tokenizerSource, std::shared_ptr callInvoker) : BaseModel(modelSource, callInvoker, Module::LoadMode::File) { - if (capabilities.empty()) { - runner_ = - std::make_unique(std::move(module_), tokenizerSource); - } else { +#ifdef RNE_ENABLE_OPENCV + if (!capabilities.empty()) { std::map> encoders; for (const auto &cap : capabilities) { if (cap == "vision") { @@ -35,7 +35,13 @@ LLM::LLM(const std::string &modelSource, const std::string &tokenizerSource, } runner_ = std::make_unique( std::move(module_), tokenizerSource, std::move(encoders)); + } else { +#endif + runner_ = + std::make_unique(std::move(module_), tokenizerSource); +#ifdef RNE_ENABLE_OPENCV } +#endif auto loadResult = runner_->load(); if (loadResult != Error::Ok) { diff --git a/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h index 50025eeeb7..ad955b29f4 100644 --- a/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h +++ b/packages/react-native-executorch/common/rnexecutorch/threads/GlobalThreadPool.h @@ -4,7 +4,9 @@ #include #include #include +#ifdef RNE_ENABLE_OPENCV #include +#endif #include #include #include @@ -41,7 +43,9 @@ class GlobalThreadPool { config); // Disable OpenCV's internal threading to prevent it from overriding our // thread pool configuration, which would cause degraded performance +#ifdef RNE_ENABLE_OPENCV cv::setNumThreads(0); +#endif }); } diff --git a/packages/react-native-executorch/package.json b/packages/react-native-executorch/package.json index c1825e393f..b8961b85c3 100644 --- a/packages/react-native-executorch/package.json +++ b/packages/react-native-executorch/package.json @@ -18,8 +18,10 @@ "*.podspec", "third-party/include", "third-party", + "!third-party/android/libs", "!third-party/ios/ExecutorchLib", - "!third-party/ios/libs/executorch", + "!third-party/ios/ExecutorchLib.xcframework", + "!third-party/ios/libs", "!ios/build", "!android/build", "!android/gradle", @@ -38,7 +40,8 @@ "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "bob build", "prepack": "cp ../../README.md ./README.md", - "postpack": "rm ./README.md" + "postpack": "rm ./README.md", + "postinstall": "node scripts/download-libs.js" }, "keywords": [ "react-native", diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec index 4094d8815d..56a3d9aced 100644 --- a/packages/react-native-executorch/react-native-executorch.podspec +++ b/packages/react-native-executorch/react-native-executorch.podspec @@ -2,6 +2,19 @@ require "json" package = JSON.parse(File.read(File.join(__dir__, "package.json"))) +# Read the build config written by the postinstall script. +# Falls back to all features enabled if the file doesn't exist. +rne_build_config_path = File.join(__dir__, "rne-build-config.json") +if File.exist?(rne_build_config_path) + require "json" + rne_build_config = JSON.parse(File.read(rne_build_config_path)) + enable_opencv = rne_build_config["enableOpencv"] != false + enable_phonemizer = rne_build_config["enablePhonemizer"] != false +else + enable_opencv = true + enable_phonemizer = true +end + Pod::Spec.new do |s| s.name = "react-native-executorch" s.version = package["version"] @@ -13,29 +26,75 @@ Pod::Spec.new do |s| s.platforms = { :ios => min_ios_version_supported } s.source = { :git => "https://github.com/software-mansion/react-native-executorch.git", :tag => "#{s.version}" } - pthreadpool_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/ios/libs/pthreadpool', __dir__) - cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/ios/libs/cpuinfo', __dir__) - phonemis_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/ios/libs/phonemis', __dir__) + cpuinfo_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/ios/libs/cpuinfo', __dir__) + phonemis_binaries_path = File.expand_path('$(PODS_TARGET_SRCROOT)/third-party/ios/libs/phonemis', __dir__) - s.user_target_xcconfig = { - "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include", + # --- Core sources (always compiled) --- + opencv_source_dirs = [ + "common/rnexecutorch/models/classification", + "common/rnexecutorch/models/object_detection", + "common/rnexecutorch/models/semantic_segmentation", + "common/rnexecutorch/models/instance_segmentation", + "common/rnexecutorch/models/style_transfer", + "common/rnexecutorch/models/ocr", + "common/rnexecutorch/models/vertical_ocr", + "common/rnexecutorch/models/embeddings/image", + "common/rnexecutorch/models/text_to_image", + "common/rnexecutorch/utils/computer_vision", + ] + opencv_source_files = opencv_source_dirs.map { |d| "#{d}/**/*.{cpp,c,h,hpp}" } + opencv_source_files += [ + "common/rnexecutorch/models/VisionModel.{cpp,h}", + "common/rnexecutorch/data_processing/ImageProcessing.{cpp,h}", + "common/rnexecutorch/utils/FrameExtractor.{cpp,h}", + "common/rnexecutorch/utils/FrameProcessor.{cpp,h}", + "common/rnexecutorch/utils/FrameTransform.{cpp,h}", + ] - "OTHER_LDFLAGS[sdk=iphoneos*]" => [ - '$(inherited)', - "\"#{pthreadpool_binaries_path}/physical-arm64-release/libpthreadpool.a\"", - "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"", - "\"#{phonemis_binaries_path}/physical-arm64-release/libphonemis.a\"", + phonemizer_source_files = [ + "common/rnexecutorch/models/text_to_speech/**/*.{cpp,c,h,hpp}", + ] - ].join(' '), + s.source_files = [ + "ios/**/*.{m,mm,h}", + "common/**/*.{cpp,c,h,hpp}", + ] + + exclude_files = [ + "common/rnexecutorch/tests/**/*.{cpp}", + "common/rnexecutorch/jsi/*.{h,hpp}", + ] + exclude_files += opencv_source_files unless enable_opencv + exclude_files += phonemizer_source_files unless enable_phonemizer + s.exclude_files = exclude_files - "OTHER_LDFLAGS[sdk=iphonesimulator*]" => [ - '$(inherited)', - "\"#{pthreadpool_binaries_path}/simulator-arm64-debug/libpthreadpool.a\"", - "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"", - "\"#{phonemis_binaries_path}/simulator-arm64-debug/libphonemis.a\"", - ].join(' '), + # --- Preprocessor flags --- + extra_compiler_flags = [] + extra_compiler_flags << "-DRNE_ENABLE_OPENCV" if enable_opencv + extra_compiler_flags << "-DRNE_ENABLE_PHONEMIZER" if enable_phonemizer + # --- Link flags --- + physical_ldflags = [ + '$(inherited)', + "\"#{pthreadpool_binaries_path}/physical-arm64-release/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"", + ] + simulator_ldflags = [ + '$(inherited)', + "\"#{pthreadpool_binaries_path}/simulator-arm64-debug/libpthreadpool.a\"", + "\"#{cpuinfo_binaries_path}/libcpuinfo.a\"", + ] + + if enable_phonemizer + physical_ldflags << "\"#{phonemis_binaries_path}/physical-arm64-release/libphonemis.a\"" + simulator_ldflags << "\"#{phonemis_binaries_path}/simulator-arm64-debug/libphonemis.a\"" + end + + s.user_target_xcconfig = { + "HEADER_SEARCH_PATHS" => "$(PODS_TARGET_SRCROOT)/third-party/include", + "OTHER_LDFLAGS[sdk=iphoneos*]" => physical_ldflags.join(' '), + "OTHER_LDFLAGS[sdk=iphonesimulator*]" => simulator_ldflags.join(' '), 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', } @@ -47,32 +106,18 @@ Pod::Spec.new do |s| '"$(PODS_TARGET_SRCROOT)/third-party/include" '+ '"$(PODS_TARGET_SRCROOT)/common" ', "CLANG_CXX_LANGUAGE_STANDARD" => "c++20", + "OTHER_CPLUSPLUSFLAGS" => extra_compiler_flags.join(' '), 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64', } - s.source_files = [ - "ios/**/*.{m,mm,h}", - "common/**/*.{cpp,c,h,hpp}", - ] - s.libraries = "z" s.ios.vendored_frameworks = "third-party/ios/ExecutorchLib.xcframework" - # Exclude file with tests to not introduce gtest dependency. - # Do not include the headers from common/rnexecutorch/jsi/ as source files. - # Xcode/Cocoapods leaks them to other pods that an app also depends on, so if - # another pod includes a header with the same name without a path by - # #include "Header.h" we get a conflict. Here, headers in jsi/ collide with - # react-native-skia. The headers are preserved by preserve_paths and - # then made available by HEADER_SEARCH_PATHS. - s.exclude_files = [ - "common/rnexecutorch/tests/**/*.{cpp}", - "common/rnexecutorch/jsi/*.{h,hpp}" - ] + s.header_mappings_dir = "common/rnexecutorch" s.header_dir = "rnexecutorch" s.preserve_paths = "common/rnexecutorch/jsi/*.{h,hpp}" - s.dependency "opencv-rne", "~> 4.11.0" + s.dependency "opencv-rne", "~> 4.11.0" if enable_opencv install_modules_dependencies(s) end diff --git a/packages/react-native-executorch/scripts/download-libs.js b/packages/react-native-executorch/scripts/download-libs.js new file mode 100644 index 0000000000..78b06d9167 --- /dev/null +++ b/packages/react-native-executorch/scripts/download-libs.js @@ -0,0 +1,279 @@ +/** + * On-demand native library downloader + * + * Runs at postinstall time. Downloads prebuilt native artifacts from GitHub Releases + * and extracts them into third-party/ so the existing CMakeLists.txt / podspec + * can find them at build time without any other changes. + * + * Artifact layout on GitHub Releases (per version tag, e.g. v0.9.0): + * + * core-android-arm64-v8a.tar.gz -- executorch, pthreadpool, cpuinfo for arm64 + * core-android-x86_64.tar.gz -- executorch for x86_64 + * core-ios.tar.gz -- ExecutorchLib.xcframework + * opencv-android-arm64-v8a.tar.gz -- OpenCV for arm64 + * opencv-android-x86_64.tar.gz -- OpenCV for x86_64 + * opencv-ios.tar.gz -- OpenCV xcframework + * phonemizer-android-arm64-v8a.tar.gz + * phonemizer-android-x86_64.tar.gz + * phonemizer-ios.tar.gz + * + * Each tarball extracts into third-party/android/libs/ or third-party/ios/ + * preserving the existing directory structure so CMakeLists/podspec need no changes. + * + * User configuration (in the app's package.json): + * "react-native-executorch": { + * "extras": ["opencv", "phonemizer"] // default: both enabled + * } + * + * Environment variables: + * RNET_SKIP_DOWNLOAD=1 -- skip download entirely (for CI with pre-cached libs) + * RNET_LIBS_CACHE_DIR=/path -- use custom cache dir instead of default + * RNET_TARGET=android-arm64 -- force specific target (skip auto-detection) + * RNET_BASE_URL=http://localhost:8080 -- override base URL (useful for local testing: + * cd dist-artifacts && python3 -m http.server 8080) + * GITHUB_TOKEN=ghp_xxx -- GitHub token for accessing draft releases + */ + +'use strict'; + +const https = require('https'); +const http = require('http'); +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +// ---- Config ---------------------------------------------------------------- + +const PACKAGE_VERSION = require('../package.json').version; +const GITHUB_REPO = 'software-mansion/react-native-executorch'; +const BASE_URL = + process.env.RNET_BASE_URL || + `https://github.com/${GITHUB_REPO}/releases/download/v${PACKAGE_VERSION}`; + +const PACKAGE_ROOT = path.resolve(__dirname, '..'); +const THIRD_PARTY_DIR = path.join(PACKAGE_ROOT, 'third-party'); + +const DEFAULT_CACHE_DIR = path.join( + require('os').homedir(), + '.cache', + 'react-native-executorch', + PACKAGE_VERSION +); +const CACHE_DIR = process.env.RNET_LIBS_CACHE_DIR || DEFAULT_CACHE_DIR; + +// ---- User config ----------------------------------------------------------- + +function readUserExtras() { + // npm/yarn set INIT_CWD to the directory where install was invoked (project root) + const projectRoot = + process.env.INIT_CWD || process.env.npm_config_local_prefix; + if (!projectRoot) { + console.warn( + '[react-native-executorch] Could not determine project root, enabling all extras.' + ); + return ['opencv', 'phonemizer']; + } + + const userPackageJsonPath = path.join(projectRoot, 'package.json'); + try { + const userPackageJson = JSON.parse( + fs.readFileSync(userPackageJsonPath, 'utf8') + ); + const rneConfig = userPackageJson['react-native-executorch'] || {}; + return rneConfig.extras ?? ['opencv', 'phonemizer']; + } catch { + console.warn( + '[react-native-executorch] Could not read app package.json, enabling all extras.' + ); + return ['opencv', 'phonemizer']; + } +} + +function writeBuildConfig(extras) { + const config = { + enableOpencv: extras.includes('opencv'), + enablePhonemizer: extras.includes('phonemizer'), + }; + fs.writeFileSync( + path.join(PACKAGE_ROOT, 'rne-build-config.json'), + JSON.stringify(config, null, 2) + ); + return config; +} + +// ---- Target detection ------------------------------------------------------ + +function detectTargets() { + if (process.env.RNET_TARGET) { + return [process.env.RNET_TARGET]; + } + + const targets = []; + if (process.platform === 'darwin') { + targets.push('ios'); + } + targets.push('android-arm64-v8a'); + if (!process.env.RNET_NO_X86_64) { + targets.push('android-x86_64'); + } + return targets; +} + +// ---- Artifact metadata ----------------------------------------------------- + +// Core artifacts are always downloaded; optional ones only if the extra is enabled. +function getArtifacts(targets, extras) { + const artifacts = []; + + for (const target of targets) { + const destDir = target.startsWith('android') + ? path.join(THIRD_PARTY_DIR, 'android', 'libs') + : path.join(THIRD_PARTY_DIR, 'ios'); + + // Core is always needed + artifacts.push(makeArtifact(`core-${target}`, destDir)); + + // iOS OpenCV is provided via CocoaPods (opencv-rne dependency), not a tarball + if (extras.includes('opencv') && target !== 'ios') { + artifacts.push(makeArtifact(`opencv-${target}`, destDir)); + } + + if (extras.includes('phonemizer')) { + artifacts.push(makeArtifact(`phonemizer-${target}`, destDir)); + } + } + + return artifacts; +} + +function makeArtifact(name, destDir) { + return { + name, + url: `${BASE_URL}/${name}.tar.gz`, + checksumUrl: `${BASE_URL}/${name}.tar.gz.sha256`, + destDir, + cacheFile: path.join(CACHE_DIR, `${name}.tar.gz`), + cacheChecksumFile: path.join(CACHE_DIR, `${name}.tar.gz.sha256`), + }; +} + +// ---- Helpers --------------------------------------------------------------- + +function ensureDir(dir) { + fs.mkdirSync(dir, { recursive: true }); +} + +function download(url, dest) { + return new Promise((resolve, reject) => { + const file = fs.createWriteStream(dest); + const get = (url) => { + const client = url.startsWith('http://') ? http : https; + const headers = {}; + if (process.env.GITHUB_TOKEN && !url.startsWith('http://')) { + headers['Authorization'] = `Bearer ${process.env.GITHUB_TOKEN}`; + } + client.get(url, { headers }, (res) => { + if (res.statusCode === 301 || res.statusCode === 302) { + return get(res.headers.location); + } + if (res.statusCode !== 200) { + return reject(new Error(`HTTP ${res.statusCode} for ${url}`)); + } + res.pipe(file); + file.on('finish', () => file.close(resolve)); + }); + }; + get(url); + file.on('error', (err) => { + fs.unlinkSync(dest); + reject(err); + }); + }); +} + +function sha256(filePath) { + const result = execSync( + `sha256sum "${filePath}" || shasum -a 256 "${filePath}"` + ); + return result.toString().split(' ')[0].trim(); +} + +function isCacheValid(artifact) { + if (!fs.existsSync(artifact.cacheFile)) return false; + if (!fs.existsSync(artifact.cacheChecksumFile)) return false; + const expectedChecksum = fs + .readFileSync(artifact.cacheChecksumFile, 'utf8') + .trim(); + const actualChecksum = sha256(artifact.cacheFile); + return expectedChecksum === actualChecksum; +} + +function extract(tarball, destDir) { + ensureDir(destDir); + execSync(`tar -xzf "${tarball}" -C "${destDir}"`); +} + +// ---- Main ------------------------------------------------------------------ + +async function main() { + if (process.env.RNET_SKIP_DOWNLOAD) { + console.log( + '[react-native-executorch] Skipping native lib download (RNET_SKIP_DOWNLOAD set)' + ); + // Still write build config so the native build knows what features are enabled + const extras = readUserExtras(); + writeBuildConfig(extras); + return; + } + + const extras = readUserExtras(); + const buildConfig = writeBuildConfig(extras); + console.log( + `[react-native-executorch] Features: opencv=${buildConfig.enableOpencv}, phonemizer=${buildConfig.enablePhonemizer}` + ); + + const targets = detectTargets(); + const artifacts = getArtifacts(targets, extras); + + ensureDir(CACHE_DIR); + + for (const artifact of artifacts) { + console.log(`[react-native-executorch] Preparing ${artifact.name}...`); + + if (isCacheValid(artifact)) { + console.log(` ✓ Cache hit, skipping download`); + } else { + console.log(` ↓ Downloading ${artifact.url}`); + await download(artifact.checksumUrl, artifact.cacheChecksumFile); + await download(artifact.url, artifact.cacheFile); + + const expectedChecksum = fs + .readFileSync(artifact.cacheChecksumFile, 'utf8') + .trim(); + const actualChecksum = sha256(artifact.cacheFile); + if (expectedChecksum !== actualChecksum) { + throw new Error( + `Checksum mismatch for ${artifact.name}: expected ${expectedChecksum}, got ${actualChecksum}` + ); + } + console.log(` ✓ Downloaded and verified`); + } + + console.log(` ↓ Extracting to ${artifact.destDir}`); + extract(artifact.cacheFile, artifact.destDir); + console.log(` ✓ Done`); + } + + console.log('[react-native-executorch] Native libs ready.'); +} + +main().catch((err) => { + console.error( + '[react-native-executorch] Failed to download native libs:', + err.message + ); + console.error( + ' You can set RNET_SKIP_DOWNLOAD=1 to skip and provide libs manually.' + ); + process.exit(1); +}); diff --git a/packages/react-native-executorch/scripts/package-release-artifacts.sh b/packages/react-native-executorch/scripts/package-release-artifacts.sh new file mode 100755 index 0000000000..08c47a180b --- /dev/null +++ b/packages/react-native-executorch/scripts/package-release-artifacts.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# package-release-artifacts.sh +# +# Packages the currently committed native libs into release artifact tarballs +# ready to be uploaded to GitHub Releases. +# +# Run from the package root (packages/react-native-executorch/): +# ./scripts/package-release-artifacts.sh +# +# Output: dist-artifacts/ +# core-android-arm64-v8a.tar.gz + .sha256 +# core-android-x86_64.tar.gz + .sha256 +# opencv-android-arm64-v8a.tar.gz + .sha256 +# opencv-android-x86_64.tar.gz + .sha256 +# phonemizer-android-arm64-v8a.tar.gz + .sha256 +# phonemizer-android-x86_64.tar.gz + .sha256 +# core-ios.tar.gz + .sha256 +# phonemizer-ios.tar.gz + .sha256 +# +# Note: iOS OpenCV is provided via CocoaPods (opencv-rne), not a tarball. +# +# Testing the download flow +# ------------------------- +# Option A — local HTTP server (no GitHub needed): +# cd dist-artifacts && python3 -m http.server 8080 +# RNET_BASE_URL=http://localhost:8080 INIT_CWD= node scripts/download-libs.js +# +# Option B — GitHub pre-release: +# gh release create v0.9.0-libs-test --prerelease --title "libs test" \ +# --notes "Test release, will be deleted." \ +# --repo software-mansion/react-native-executorch +# gh release upload v0.9.0-libs-test dist-artifacts/* \ +# --repo software-mansion/react-native-executorch +# RNET_BASE_URL=https://github.com/software-mansion/react-native-executorch/releases/download/v0.9.0-libs-test \ +# INIT_CWD= node scripts/download-libs.js +# # cleanup: +# gh release delete v0.9.0-libs-test --repo software-mansion/react-native-executorch --yes + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +ANDROID_LIBS="$PACKAGE_ROOT/third-party/android/libs" +IOS_DIR="$PACKAGE_ROOT/third-party/ios" +OUT="$PACKAGE_ROOT/dist-artifacts" + +VERSION=$(node -p "require('$PACKAGE_ROOT/package.json').version") + +echo "Packaging release artifacts for v$VERSION" +mkdir -p "$OUT" + +# ---- Helpers ---------------------------------------------------------------- + +package() { + local name=$1 + local src_dir=$2 + local out_file="$OUT/$name.tar.gz" + + echo " → $name" + + if [ ! -d "$src_dir" ]; then + echo " ✗ Source directory not found: $src_dir" >&2 + exit 1 + fi + + tar -czf "$out_file" -C "$src_dir" . + shasum -a 256 "$out_file" | awk '{print $1}' > "$out_file.sha256" + echo " ✓ $(du -sh "$out_file" | cut -f1)" +} + +# Packages multiple source directories into a single tarball by staging them +# into a temp directory first, preserving relative paths. +package_merged() { + local name=$1 + shift + local out_file="$OUT/$name.tar.gz" + local tmp + tmp=$(mktemp -d) + + echo " → $name" + + while [[ $# -gt 0 ]]; do + local rel_path=$1 # relative path inside the tarball + local src=$2 # source directory to copy from + shift 2 + + if [ ! -d "$src" ]; then + echo " ✗ Source directory not found: $src" >&2 + rm -rf "$tmp" + exit 1 + fi + + mkdir -p "$tmp/$rel_path" + cp -r "$src/." "$tmp/$rel_path/" + done + + tar -czf "$out_file" -C "$tmp" . + shasum -a 256 "$out_file" | awk '{print $1}' > "$out_file.sha256" + echo " ✓ $(du -sh "$out_file" | cut -f1)" + rm -rf "$tmp" +} + +# ---- Android ---------------------------------------------------------------- + +echo "" +echo "Android:" + +package_merged "core-android-arm64-v8a" \ + "executorch/arm64-v8a" "$ANDROID_LIBS/executorch/arm64-v8a" \ + "pthreadpool/arm64-v8a" "$ANDROID_LIBS/pthreadpool/arm64-v8a" \ + "cpuinfo/arm64-v8a" "$ANDROID_LIBS/cpuinfo/arm64-v8a" + +package_merged "core-android-x86_64" \ + "executorch/x86_64" "$ANDROID_LIBS/executorch/x86_64" + +package_merged "opencv-android-arm64-v8a" \ + "opencv/arm64-v8a" "$ANDROID_LIBS/opencv/arm64-v8a" \ + "opencv-third-party/arm64-v8a" "$ANDROID_LIBS/opencv-third-party/arm64-v8a" + +package_merged "opencv-android-x86_64" \ + "opencv/x86_64" "$ANDROID_LIBS/opencv/x86_64" + +package_merged "phonemizer-android-arm64-v8a" \ + "phonemis/arm64-v8a" "$ANDROID_LIBS/phonemis/arm64-v8a" + +package_merged "phonemizer-android-x86_64" \ + "phonemis/x86_64" "$ANDROID_LIBS/phonemis/x86_64" + +# ---- iOS -------------------------------------------------------------------- +# Note: OpenCV for iOS is provided by CocoaPods (opencv-rne dependency). +# No opencv-ios tarball is needed. + +echo "" +echo "iOS:" + +package_merged "core-ios" \ + "ExecutorchLib.xcframework" "$IOS_DIR/ExecutorchLib.xcframework" \ + "libs/executorch" "$IOS_DIR/libs/executorch" \ + "libs/pthreadpool" "$IOS_DIR/libs/pthreadpool" \ + "libs/cpuinfo" "$IOS_DIR/libs/cpuinfo" + +package_merged "phonemizer-ios" \ + "libs/phonemis" "$IOS_DIR/libs/phonemis" + +# ---- Summary ---------------------------------------------------------------- + +echo "" +echo "Done. Artifacts written to dist-artifacts/:" +ls -lh "$OUT" +echo "" +echo "Upload these files to the GitHub Release for v$VERSION:" +echo " https://github.com/software-mansion/react-native-executorch/releases/tag/v$VERSION" diff --git a/packages/react-native-executorch/src/index.ts b/packages/react-native-executorch/src/index.ts index 3f6b9fc4ce..34fc71a592 100644 --- a/packages/react-native-executorch/src/index.ts +++ b/packages/react-native-executorch/src/index.ts @@ -110,25 +110,21 @@ declare global { } // eslint-disable no-var -if ( - global.loadStyleTransfer == null || - global.loadSemanticSegmentation == null || - global.loadInstanceSegmentation == null || - global.loadTextToImage == null || - global.loadExecutorchModule == null || - global.loadClassification == null || - global.loadObjectDetection == null || - global.loadTokenizerModule == null || - global.loadTextEmbeddings == null || - global.loadImageEmbeddings == null || - global.loadVAD == null || - global.loadLLM == null || - global.loadSpeechToText == null || - global.loadTextToSpeechKokoro == null || - global.loadOCR == null || - global.loadVerticalOCR == null || - global.__rne_isEmulator == null -) { +// Core globals are always installed regardless of which extras are enabled. +// Optional globals (opencv/phonemizer) may be absent if the library was built +// without those features — calling them at that point throws a runtime error +// from the native side with a clear message. +const CORE_GLOBALS = [ + 'loadExecutorchModule', + 'loadTokenizerModule', + 'loadLLM', + 'loadSpeechToText', + 'loadTextEmbeddings', + 'loadVAD', + '__rne_isEmulator', +] as const; + +if (CORE_GLOBALS.some((name) => global[name] == null)) { if (!ETInstallerNativeModule) { throw new Error( `Failed to install react-native-executorch: The native module could not be found.`