diff --git a/.cspell-wordlist.txt b/.cspell-wordlist.txt
index 4e68fbb607..1b9b5551f0 100644
--- a/.cspell-wordlist.txt
+++ b/.cspell-wordlist.txt
@@ -1,6 +1,15 @@
multimodal
swmansion
executorch
+RNET
+libexecutorch
+libxnnpack
+libvulkan
+xcframework
+metallib
+ASPP
+pthreadpool
+cpuinfo
execu
Execu
torch
@@ -244,4 +253,23 @@ pushd
popd
yolov
YOLOV
+DRNE
+xcframeworks
+Kleidi
+headeronly
+absl
+nlohmann
+libopencv
+Slurper
+dlopen
+fopenmp
+CPLUSPLUSFLAGS
+LDFLAGS
+iphoneos
+libbackend
XLARGE
+flatbuffer
+libthreadpool
+fxdiv
+pcre
+libkleidicv
diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml
index bb7dd51bde..c13da5b405 100644
--- a/.github/actions/setup/action.yml
+++ b/.github/actions/setup/action.yml
@@ -25,3 +25,8 @@ runs:
if: steps.yarn-cache.outputs.cache-hit != 'true'
run: yarn install --immutable
shell: bash
+ # CI is TypeScript-only and has no GitHub Release to fetch native libs
+ # from; skip the postinstall native-lib download (it still writes
+ # rne-build-config.json so the native build config exists).
+ env:
+ RNET_SKIP_DOWNLOAD: '1'
diff --git a/apps/computer-vision/package.json b/apps/computer-vision/package.json
index 9f1ce3f11f..ef9f1f2005 100644
--- a/apps/computer-vision/package.json
+++ b/apps/computer-vision/package.json
@@ -2,6 +2,14 @@
"name": "computer-vision",
"version": "1.0.0",
"main": "expo-router/entry",
+ "react-native-executorch": {
+ "features": [
+ "classification",
+ "keypointDetection",
+ "semanticSegmentation",
+ "styleTransfer"
+ ]
+ },
"scripts": {
"start": "expo start",
"android": "expo run:android",
diff --git a/apps/nlp/package.json b/apps/nlp/package.json
index 973f442349..361bd18f48 100644
--- a/apps/nlp/package.json
+++ b/apps/nlp/package.json
@@ -2,6 +2,11 @@
"name": "nlp",
"version": "1.0.0",
"main": "expo-router/entry",
+ "react-native-executorch": {
+ "features": [
+ "tokenizer"
+ ]
+ },
"scripts": {
"start": "expo start",
"android": "expo run:android",
diff --git a/docs/docs/01-fundamentals/01-getting-started.md b/docs/docs/01-fundamentals/01-getting-started.md
new file mode 100644
index 0000000000..3c9e2c5603
--- /dev/null
+++ b/docs/docs/01-fundamentals/01-getting-started.md
@@ -0,0 +1,266 @@
+---
+title: Getting Started
+slug: /fundamentals/getting-started
+keywords:
+ [
+ react native,
+ react native ai,
+ react native llm,
+ react native qwen,
+ react native llama,
+ react native executorch,
+ executorch,
+ on-device ai,
+ pytorch,
+ mobile ai,
+ ]
+description: 'Get started with React Native ExecuTorch - a framework for running AI models on-device in your React Native applications.'
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+## What is ExecuTorch?
+
+[ExecuTorch](https://executorch.ai) is a novel AI framework developed by Meta, designed to streamline deploying PyTorch models on a variety of devices, including mobile phones and microcontrollers. This framework enables exporting models into standalone binaries, allowing them to run locally without requiring API calls. ExecuTorch achieves state-of-the-art performance through optimizations and delegates such as Core ML and XNNPACK. It provides a seamless export process with robust debugging options, making it easier to resolve issues if they arise.
+
+## React Native ExecuTorch
+
+React Native ExecuTorch is our way of bringing ExecuTorch into the React Native world. Our API is built to be simple, declarative, and efficient. Additionally, we provide a set of pre-exported models for common use cases, so you don't have to worry about handling exports yourself. With just a few lines of JavaScript, you can run AI models (even LLMs π) right on your deviceβkeeping user data private and saving on cloud costs.
+
+## Compatibility
+
+React Native Executorch supports only the [New React Native architecture](https://reactnative.dev/architecture/landing-page).
+
+If your app still runs on the old architecture, please consider upgrading to the New Architecture.
+
+For supported React Native and Expo versions, see the [Compatibility table](https://docs.swmansion.com/react-native-executorch/docs/other/compatibility).
+
+## Installation
+
+Installation takes two steps: install the core package, then install a resource fetcher adapter that matches your project type. If you want to implement your own model fetching logic instead, see [this document](https://docs.swmansion.com/react-native-executorch/docs/resource-fetcher/custom-adapter).
+
+### 1. Install the core package
+
+
+
+
+ ```bash
+ npm install react-native-executorch
+ ```
+
+
+
+
+ ```bash
+ pnpm add react-native-executorch
+ ```
+
+
+
+
+ ```bash
+ yarn add react-native-executorch
+ ```
+
+
+
+
+At the end of installation a `postinstall` step downloads the native binaries your app needs (see [Selecting native libraries](#selecting-native-libraries) below). With no extra configuration it downloads everything, so you can start immediately.
+
+### 2. Install a resource fetcher
+
+Pick the adapter that matches your project. We recommend the Expo adapter when your app uses Expo; use the bare adapter for projects without Expo.
+
+#### Expo projects
+
+
+
+
+ ```bash
+ npm install react-native-executorch-expo-resource-fetcher expo-file-system expo-asset
+ ```
+
+
+
+
+ ```bash
+ pnpm add react-native-executorch-expo-resource-fetcher expo-file-system expo-asset
+ ```
+
+
+
+
+ ```bash
+ yarn add react-native-executorch-expo-resource-fetcher expo-file-system expo-asset
+ ```
+
+
+
+
+#### Bare React Native projects
+
+
+
+
+ ```bash
+ npm install react-native-executorch-bare-resource-fetcher @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader
+ ```
+
+
+
+
+ ```bash
+ pnpm add react-native-executorch-bare-resource-fetcher @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader
+ ```
+
+
+
+
+ ```bash
+ yarn add react-native-executorch-bare-resource-fetcher @dr.pogodin/react-native-fs @kesha-antonov/react-native-background-downloader
+ ```
+
+
+
+
+:::warning
+Before using any other API, you must call `initExecutorch` with a resource fetcher adapter at the entry point of your app:
+
+```js
+import { initExecutorch } from 'react-native-executorch';
+import { ExpoResourceFetcher } from 'react-native-executorch-expo-resource-fetcher';
+// or BareResourceFetcher for bare react-native projects
+
+initExecutorch({ resourceFetcher: ExpoResourceFetcher });
+```
+
+Calling any library API without initializing first will throw a `ResourceFetcherAdapterNotInitialized` error.
+:::
+
+Our library offers support for both bare React Native and Expo projects. Please follow the instructions from [Loading models section](https://docs.swmansion.com/react-native-executorch/docs/fundamentals/loading-models) to make sure you setup your project correctly. We encourage you to use Expo project if possible. If you are planning to migrate from bare React Native to Expo project, the link (https://docs.expo.dev/bare/installing-expo-modules/) offers a guidance on setting up Expo Modules in a bare React Native environment.
+
+If you plan on using your models via require() instead of fetching them from a url, you also need to add following lines to your `metro.config.js`:
+
+```json
+// metro.config.js
+...
+ defaultConfig.resolver.assetExts.push('pte')
+ defaultConfig.resolver.assetExts.push('bin')
+...
+```
+
+This allows us to use binaries, such as exported models or tokenizers for LLMs.
+
+:::warning
+When using Expo, please note that you need to use a custom development build of your app, not the standard Expo Go app. This is because we rely on native modules, which Expo Go doesnβt support.
+:::
+
+:::info
+Because we are using ExecuTorch under the hood, you won't be able to build iOS app for release with simulator selected as the target device. Make sure to test release builds on real devices.
+:::
+
+Running the app with the library:
+
+
+
+
+ ```bash
+ npm run -- -d
+ ```
+
+
+
+
+ ```bash
+ pnpm -d
+ ```
+
+
+
+
+ ```bash
+ yarn -d
+ ```
+
+
+
+
+## Selecting native libraries
+
+The native binaries React Native ExecuTorch relies on β the ExecuTorch runtime, the hardware backends (XNNPACK, Core ML, MLX, Vulkan), and OpenCV β are **downloaded on demand at install time** rather than bundled into the npm package. A `postinstall` script reads an optional `react-native-executorch` block from your app's `package.json`, fetches only the binaries your app needs, and the native build then links only those.
+
+If you omit the block, **every backend and library is downloaded and enabled** β no configuration is required to get started. Trimming the set keeps your app smaller and your builds faster.
+
+### Opting into a subset
+
+Declare what you use with any combination of `features`, `backends`, and `libs`:
+
+```json
+// package.json
+{
+ "react-native-executorch": {
+ "features": ["classification", "styleTransfer"]
+ }
+}
+```
+
+- **`features`** β high-level model tasks. Each one expands to the backends and libraries it needs (see the table below). This is the recommended way to configure.
+- **`backends`** β request hardware backends directly: `xnnpack`, `coreml` (iOS), `mlx` (iOS), `vulkan` (Android).
+- **`libs`** β request extra native libraries directly: `opencv`, `phonemis`.
+
+The three lists are merged, so you can pair a `features` set with an extra `backends` / `libs` entry. After editing the block, re-run your package manager's install to re-provision (`yarn install`).
+
+### Feature β backend / lib mapping
+
+These tasks are available today:
+
+| Feature | Backends | Extra libs |
+| --- | --- | --- |
+| `classification` | xnnpack, coreml | opencv |
+| `semanticSegmentation` | xnnpack | opencv |
+| `styleTransfer` | xnnpack, coreml | opencv |
+| `keypointDetection` | xnnpack, coreml, mlx | opencv |
+| `tokenizer` | β | β |
+
+The map also contains forward-looking entries (`llm`, `multimodalLLM`, `speechToText`, `objectDetection`, `ocr`, β¦) for tasks that are not yet exposed in the JS API; requesting one provisions the right binaries but has no hook to call yet.
+
+### Platform notes
+
+- **Core ML** is iOS-only. **MLX** is iOS-only and ships the **device slice only** β the iOS simulator cannot drive MLX-on-Metal, so test MLX-backed models on a physical device.
+- **Vulkan** is Android-only.
+- **OpenCV** is provided on iOS through the `opencv-rne` CocoaPod and on Android as static libraries; any vision feature pulls it in automatically.
+- On CI, set `RNET_SKIP_DOWNLOAD=1` to skip the network download. The `rne-build-config.json` file is still written so the native build can resolve its feature flags.
+
+For how these artifacts are produced, shipped, and stitched into a build, see [Native libraries & backend splitting](./02-native-libraries.md).
+
+## Building from source
+
+To build the library from source instead, clone the repository and initialize submodules:
+
+```bash
+git clone -b release/0.9 https://github.com/software-mansion/react-native-executorch.git
+cd react-native-executorch
+
+git submodule update --init --recursive packages/react-native-executorch/third-party/common
+
+yarn
+```
+
+## Supporting new models in React Native ExecuTorch
+
+Adding new functionality to the library follows a consistent three-step integration pipeline:
+
+1. **Model Serialization:** Export PyTorch model for a specific task (e.g. object detection) into the `*.pte` format, which is optimized for the ExecuTorch runtime.
+
+2. **Native Implementation:** Develop a C++ execution layer that interfaces with the ExecuTorch runtime to handle inference. This layer also manages model-dependent logic, such as data pre-processing and post-processing.
+
+3. **TS Bindings:** Finally, implement a TypeScript API that bridges the JavaScript environment to the native C++ logic, providing a clean, typed interface for the end user.
+
+## Good reads
+
+If you want to dive deeper into ExecuTorch or our previous work with the framework, we highly encourage you to check out the following resources:
+
+- [ExecuTorch docs](https://pytorch.org/executorch/stable/index.html)
+- [React Native RAG](https://blog.swmansion.com/introducing-react-native-rag-fbb62efa4991)
+- [Offline Text Recognition on Mobile: How We Brought EasyOCR to React Native ExecuTorch](https://blog.swmansion.com/bringing-easyocr-to-react-native-executorch-2401c09c2d0c)
diff --git a/docs/docs/01-fundamentals/02-native-libraries.md b/docs/docs/01-fundamentals/02-native-libraries.md
new file mode 100644
index 0000000000..6fce102e0a
--- /dev/null
+++ b/docs/docs/01-fundamentals/02-native-libraries.md
@@ -0,0 +1,183 @@
+---
+title: Native Libraries & Backend Splitting
+slug: /fundamentals/native-libraries
+description: 'How React Native ExecuTorch produces, ships, and links its native binaries on demand, with each ExecuTorch backend split into its own downloadable artifact.'
+keywords:
+ [
+ react native executorch,
+ executorch,
+ native libraries,
+ backend splitting,
+ xnnpack,
+ coreml,
+ mlx,
+ vulkan,
+ on-device ai,
+ ]
+---
+
+This page explains how the native dependencies (the ExecuTorch runtime, the hardware backends, OpenCV) are produced, shipped, and stitched into an app build. For the app-author summary β how to opt into a subset β see the [Selecting native libraries](./01-getting-started.md#selecting-native-libraries) section of Getting Started. This page is aimed at maintainers and anyone curious about how the split works under the hood.
+
+## Why split the backends?
+
+Every backend (XNNPACK, Core ML, MLX, Vulkan) and OpenCV adds size and build time. Instead of bundling one monolithic native library with everything baked in, each backend ships as its **own downloadable artifact**, and an app pulls only the ones it declares. The result is smaller apps and faster builds, with the full set still available by default.
+
+## High-level flow
+
+```
+ ββββββββββββββββββββββββ ββββββββββββββββββββββββββ βββββββββββββββββββββββββ
+ β ExecuTorch fork β ββββΆ β GitHub Release v β ββββΆ β postinstall script β
+ β @ms/separate- β β .tar.gz β β download-libs.js β
+ β backends β β .tar.gz.256 β β β
+ ββββββββββββββββββββββββ ββββββββββββββββββββββββββ βββββββββββββ¬ββββββββββββ
+ β
+ βΌ
+ βββββββββββββββββββββββββ
+ β third-party/android β
+ β third-party/ios β
+ β third-party/include β
+ β rne-build-config.jsonβ
+ βββββββββββββ¬ββββββββββββ
+ β
+ βββββββββββββββββββββββββββββββββββ΄ββββββββββββββββββββββββββββ
+ βΌ βΌ
+ βββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
+ β android/ β β react-native-executorch β
+ β build.gradle.kts β β .podspec β
+ β + CMakeLists.txt β β RNE_ENABLE_* β
+ β -DRNE_ENABLE_* β β force_load xcframeworks β
+ βββββββββββββββββββββββββ ββββββββββββββββββββββββββββ
+```
+
+## Install-time: `scripts/download-libs.js`
+
+Runs at `postinstall`. Responsibilities:
+
+1. Read `react-native-executorch.{backends, libs, features}` from the app's `package.json` (via `INIT_CWD`). Each array is optional; `features` is expanded through `FEATURE_MAP` into (backends, libs) and merged with the explicit arrays. With no config, everything defaults to enabled. The legacy `extras` field is rejected with a migration error.
+2. Write `rne-build-config.json` at the package root with boolean flags (`enableXnnpack`, `enableCoreml`, `enableMlx`, `enableVulkan`, `enableOpencv`, `enablePhonemis`). This file is the single source of truth consumed by both the Gradle build and the podspec.
+3. Detect targets (`ios` on macOS; always `android-arm64-v8a` and, unless `RNET_NO_X86_64` is set, `android-x86_64`).
+4. For each target Γ enabled backend/lib, fetch the matching `.tar.gz` from the GitHub Release tagged `v`, verify its `.sha256`, and extract it into `third-party/`. Platform-independent headers ride in a single always-downloaded `headers.tar.gz`.
+5. Cache validated tarballs under `~/.cache/react-native-executorch//` so later installs skip the network.
+
+Environment overrides: `RNET_SKIP_DOWNLOAD`, `RNET_HEADERS_ONLY` (fetch only `headers.tar.gz`, no native libs β e.g. for clang-tidy / IDE tooling), `RNET_LIBS_CACHE_DIR`, `RNET_TARGET`, `RNET_NO_X86_64`, `RNET_BASE_URL` (point it at a local `python3 -m http.server` serving `dist-artifacts/` for local iteration), and `GITHUB_TOKEN` (needed for draft releases).
+
+The artifacts per target:
+
+| Artifact name | Target | Contents |
+| ------------------------ | ------- | -------------------------------------------------------------- |
+| `headers` | any | ExecuTorch + c10/torch + tokenizer + OpenCV headers (platform-independent) |
+| `core-android-arm64-v8a` | Android | `libexecutorch.so` (no backends) + the ABI-independent `executorch.jar` |
+| `core-android-x86_64` | Android | x86_64 `libexecutorch.so` for the emulator |
+| `xnnpack-android-*` | Android | `libxnnpack_executorch_backend.so` (separately loaded) |
+| `vulkan-android-*` | Android | `libvulkan_executorch_backend.so` (separately loaded) |
+| `opencv-android-*` | Android | Static OpenCV + KleidiCV HAL |
+| `core-ios` | iOS | `ExecutorchLib.xcframework` + merged ExecuTorch `.a` slices |
+| `xnnpack-ios` | iOS | `XnnpackBackend.xcframework` |
+| `coreml-ios` | iOS | `CoreMLBackend.xcframework` |
+| `mlx-ios` | iOS | `MLXBackend.xcframework` (device slice only) + `mlx.metallib` |
+
+iOS OpenCV is **not** a tarball β it is consumed through the `opencv-rne` CocoaPod. `phonemis` has **no** tarball either β it is a git submodule at `third-party/common/phonemis` compiled from source when enabled.
+
+### Header provenance
+
+`headers.tar.gz` is assembled by `scripts/vendor-headers.sh`, which is needed because the executorch header surface spans **four** sources β a copy of the CMake install tree alone is incomplete (it omits the source-only headers such as `extension/llm/{runner,custom_ops,apple}`, which the rewrite's LLM/multimodal tasks compile against directly):
+
+1. **ExecuTorch C++ source headers** (`runtime`, `extension`, `kernels`, β¦ from the executorch checkout) β the full public surface incl. the LLM runner helpers and the bundled tokenizer third-party (`abseil-cpp`/`re2`/`json`/β¦).
+2. **Build-generated / installed headers** (`cmake-out*/include`) β flatbuffer `*_generated.h` and codegen'd `kernels/*/Functions.h`.
+3. **c10 / torch** from the assembled `executorch.xcframework` public headers.
+4. **opencv2** from the OpenCV prebuilt (same source as the `opencv-rne` pod), since OpenCV is not built from executorch.
+
+Run it before `package-release-artifacts.sh`:
+
+```bash
+./scripts/vendor-headers.sh
+```
+
+Headers are **downloaded, not committed**.
+
+## Build-time: Android
+
+`android/build.gradle.kts` reads `rne-build-config.json` once (via `JsonSlurper`, falling back to all-on if the file is missing) and forwards the booleans to CMake:
+
+```kotlin
+"-DRNE_ENABLE_OPENCV=${rneFlag("enableOpencv")}",
+"-DRNE_ENABLE_PHONEMIS=${rneFlag("enablePhonemis")}",
+"-DRNE_ENABLE_XNNPACK=${rneFlag("enableXnnpack")}",
+"-DRNE_ENABLE_VULKAN=${rneFlag("enableVulkan")}"
+```
+
+It also honours the app's `reactNativeArchitectures` Gradle property, so a device build that requests only `arm64-v8a` provisions and links only that ABI.
+
+`android/CMakeLists.txt` responds by:
+
+- Adding `-DRNE_ENABLE_OPENCV` / `-DRNE_ENABLE_PHONEMIS` compile definitions so C++ code can `#ifdef` around optional dependencies.
+- Compiling the OpenCV-dependent source group and linking the static `libopencv_*.a` + KleidiCV HAL (arm64 only) when `RNE_ENABLE_OPENCV=ON`.
+- Always importing and linking the prebuilt `libexecutorch.so` from `third-party/android/libs/executorch//`.
+- When `RNE_ENABLE_XNNPACK=ON` / `RNE_ENABLE_VULKAN=ON`, importing the matching `libxnnpack_executorch_backend.so` / `libvulkan_executorch_backend.so` and linking against it. Linking (rather than `dlopen`) lets Gradle bundle the `.so` into the APK and makes the dynamic linker load it whenever the main library loads β each backend's load-time constructor then registers itself with the runtime in `libexecutorch.so`.
+- Statically linking the OpenMP runtime (`-fopenmp -static-openmp`) to resolve the optimized-kernel symbols `libexecutorch.so` references.
+
+## Build-time: iOS
+
+`react-native-executorch.podspec` reads the same `rne-build-config.json` and:
+
+- Excludes the OpenCV C++ source group from compilation when `enableOpencv` is false.
+- Appends `-DRNE_ENABLE_*` to `OTHER_CPLUSPLUSFLAGS`.
+- Assembles `OTHER_LDFLAGS` with a `-force_load` entry for each enabled backend xcframework. MLX is force-loaded on the **device slice only** (`sdk=iphoneos*`).
+- Vendors `ExecutorchLib.xcframework` only β the backend xcframeworks live on the linker command line, never in the CocoaPods vendoring list (see below for why).
+- Adds the `opencv-rne` pod dependency, the `CoreML` / `Metal` system frameworks, and the `mlx.metallib` bundle resource conditionally, based on which backends are enabled.
+
+## Why backends must be force-loaded
+
+ExecuTorch registers kernels statically via `__attribute__((constructor))` functions inside each backend's `.a` / `.so`. Two consequences:
+
+1. **Force-load is required.** Linkers drop unreferenced object files. The registrar symbols have no external callers (they run as global constructors at load time), so a plain link keeps the backend library on disk but strips the registration symbols β and the app then fails with `Missing operator: ...` at inference. Every backend library must be force-loaded (`-force_load` on iOS, `--whole-archive` on Android).
+2. **Exactly one copy of each CPU-kernel registration must exist.** Multiple backend libraries that each whole-archive-link the CPU ops cause duplicate-registration aborts when both are force-loaded into the same process.
+
+On **iOS** each backend ships as its own static xcframework and the podspec force-loads only the opted-in ones; `ExecutorchLib.xcframework` itself does not whole-archive the CPU ops, so there is no duplication. On **Android** the fork builds each backend as a standalone shared library (`EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED` / `EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED`) that links only its own archive plus `executorch_core` β no kernel-registration archives β so loading it on top of `libexecutorch.so` duplicates nothing.
+
+## Building the artifacts
+
+The binaries come from the ExecuTorch fork [`software-mansion-labs/executorch@ms/separate-backends`](https://github.com/software-mansion-labs/executorch/tree/@ms/separate-backends), which is already on ExecuTorch **1.3.1** (the same version as `main`). Bumping the `react-native-executorch` package version means re-rolling the Release artifacts from the corresponding fork commit.
+
+> **MLX-iOS note.** Building the iOS MLX backend requires the MLX-iOS work that lives in the `@nk/mlx-ios` line. That branch merges into `@ms/separate-backends` conflict-free; after the merge a single `build_apple_frameworks.sh --Release` pass produces the full set including a real `libbackend_mlx_ios.a` and `mlx.metallib`. Only the **device** slice is built and shipped β the iOS simulator cannot drive MLX-on-Metal.
+
+### Android
+
+From the fork (with `@ms/separate-backends` checked out), per ABI:
+
+```bash
+export ANDROID_NDK=$HOME/Library/Android/sdk/ndk/27.1.12297006
+EXECUTORCH_BUILD_VULKAN=ON \
+EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED=ON \
+EXECUTORCH_BUILD_XNNPACK_BACKEND_SHARED=ON \
+ANDROID_ABI=arm64-v8a ./scripts/build_android_library.sh # repeat with x86_64
+```
+
+This emits `libexecutorch.so`, `libxnnpack_executorch_backend.so`, and `libvulkan_executorch_backend.so`. Strip each with the NDK `llvm-strip` before packaging.
+
+### iOS
+
+```bash
+rm -rf cmake-out
+./scripts/build_apple_frameworks.sh --Release
+```
+
+This produces the merged per-slice `.a` archives. RNE's `third-party/ios/ExecutorchLib/build.sh` then repackages them into `ExecutorchLib.xcframework` plus the per-backend xcframeworks (`XnnpackBackend`, `CoreMLBackend`, `MLXBackend`). CocoaPods requires the library file name to be identical across an xcframework's slices, which is why `build.sh` renames each slice before calling `xcodebuild -create-xcframework`.
+
+### Packaging and uploading
+
+Stage the built outputs into `third-party/`, then run:
+
+```bash
+./scripts/package-release-artifacts.sh
+```
+
+This writes every `.tar.gz` + `.sha256` into `dist-artifacts/`. Upload all of them as assets on the `v` GitHub Release. To test the download flow before publishing, serve the directory locally and point the script at it:
+
+```bash
+cd packages/react-native-executorch/dist-artifacts
+python3 -m http.server 8080 &
+RNET_BASE_URL=http://localhost:8080 yarn install
+```
+
+The same checksum verification runs, so a stale cache is still rejected.
diff --git a/docs/docs/01-fundamentals/_category_.json b/docs/docs/01-fundamentals/_category_.json
new file mode 100644
index 0000000000..e3fddcbebd
--- /dev/null
+++ b/docs/docs/01-fundamentals/_category_.json
@@ -0,0 +1,6 @@
+{
+ "label": "Fundamentals",
+ "link": {
+ "type": "generated-index"
+ }
+}
diff --git a/docs/sidebars.js b/docs/sidebars.js
index 76e1313467..344cc03aed 100644
--- a/docs/sidebars.js
+++ b/docs/sidebars.js
@@ -20,37 +20,41 @@ const sidebars = {
label: 'Fundamentals',
items: [{ type: 'autogenerated', dirName: '01-fundamentals' }],
},
- {
- type: 'category',
- label: 'Benchmarks',
- items: [{ type: 'autogenerated', dirName: '02-benchmarks' }],
- },
- {
- type: 'category',
- label: 'Hooks',
- items: [{ type: 'autogenerated', dirName: '03-hooks' }],
- },
- {
- type: 'category',
- label: 'Typescript API',
- items: [{ type: 'autogenerated', dirName: '04-typescript-api' }],
- },
- {
- type: 'category',
- label: 'Utilities',
- items: [{ type: 'autogenerated', dirName: '05-utilities' }],
- },
- {
- type: 'category',
- label: 'Resource Fetcher',
- items: [{ type: 'autogenerated', dirName: '08-resource-fetcher' }],
- },
- {
- type: 'category',
- label: 'Other',
- // 07 to avoid conflict with 06-api-reference which is auto-generated and not tracked by git
- items: [{ type: 'autogenerated', dirName: '07-other' }],
- },
+ // The remaining sections (Benchmarks, Hooks, Typescript API, Utilities,
+ // Resource Fetcher, Other, API Reference) are being ported into the rewrite
+ // incrementally. Re-enable each category here as its `docs/docs/` is
+ // restored, so the current-version site builds with only the dirs present.
+ // {
+ // type: 'category',
+ // label: 'Benchmarks',
+ // items: [{ type: 'autogenerated', dirName: '02-benchmarks' }],
+ // },
+ // {
+ // type: 'category',
+ // label: 'Hooks',
+ // items: [{ type: 'autogenerated', dirName: '03-hooks' }],
+ // },
+ // {
+ // type: 'category',
+ // label: 'Typescript API',
+ // items: [{ type: 'autogenerated', dirName: '04-typescript-api' }],
+ // },
+ // {
+ // type: 'category',
+ // label: 'Utilities',
+ // items: [{ type: 'autogenerated', dirName: '05-utilities' }],
+ // },
+ // {
+ // type: 'category',
+ // label: 'Resource Fetcher',
+ // items: [{ type: 'autogenerated', dirName: '08-resource-fetcher' }],
+ // },
+ // {
+ // type: 'category',
+ // label: 'Other',
+ // // 07 to avoid conflict with 06-api-reference which is auto-generated and not tracked by git
+ // items: [{ type: 'autogenerated', dirName: '07-other' }],
+ // },
{
type: 'category',
label: 'API Reference',
diff --git a/packages/react-native-executorch/.gitignore b/packages/react-native-executorch/.gitignore
new file mode 100644
index 0000000000..4498ff073d
--- /dev/null
+++ b/packages/react-native-executorch/.gitignore
@@ -0,0 +1,6 @@
+# Generated by scripts/download-libs.js at postinstall (records which backends/libs
+# are enabled; consumed by the podspec and android/build.gradle.kts).
+rne-build-config.json
+
+# Generated by scripts/package-release-artifacts.sh
+dist-artifacts/
diff --git a/packages/react-native-executorch/android/CMakeLists.txt b/packages/react-native-executorch/android/CMakeLists.txt
index 75006a676a..a14fb9625b 100644
--- a/packages/react-native-executorch/android/CMakeLists.txt
+++ b/packages/react-native-executorch/android/CMakeLists.txt
@@ -1,55 +1,130 @@
-cmake_minimum_required(VERSION 3.10)
+cmake_minimum_required(VERSION 3.13)
project(RnExecutorch)
set(CMAKE_VERBOSE_MAKEFILE ON)
set(CMAKE_CXX_STANDARD 20)
-set(EXEC_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/android/jniLibs/${ANDROID_ABI}/libexecutorch.so")
-set(OPENCV_LIB_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../third-party/android/jniLibs/${ANDROID_ABI}/libopencv_java4.so")
+set(THIRD_PARTY_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../third-party")
+set(LIBS_DIR "${THIRD_PARTY_DIR}/android/libs")
+set(INCLUDE_DIR "${THIRD_PARTY_DIR}/include")
+set(CPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../cpp")
-add_library(executorch SHARED IMPORTED)
-set_target_properties(executorch PROPERTIES IMPORTED_LOCATION ${EXEC_LIB_PATH})
-
-add_library(opencv SHARED IMPORTED)
-set_target_properties(opencv PROPERTIES IMPORTED_LOCATION ${OPENCV_LIB_PATH})
+# Feature flags forwarded from build.gradle.kts (which reads rne-build-config.json
+# written by scripts/download-libs.js). Default ON so a manual/full provisioning
+# still builds everything.
+option(RNE_ENABLE_OPENCV "Compile + link OpenCV-dependent sources" ON)
+option(RNE_ENABLE_PHONEMIS "Compile phonemis (TTS) sources" ON)
+option(RNE_ENABLE_XNNPACK "Link the XNNPACK backend shared library" ON)
+option(RNE_ENABLE_VULKAN "Link the Vulkan backend shared library" ON)
find_package(ReactAndroid REQUIRED CONFIG)
-file(GLOB CORE_SOURCES ../cpp/core/*.cpp)
-file(GLOB MATH_SOURCES ../cpp/extensions/math/*.cpp)
-file(GLOB CV_SOURCES ../cpp/extensions/cv/*.cpp)
-file(GLOB NLP_SOURCES ../cpp/extensions/nlp/*.cpp)
+# --- Source groups ---
+# core/math/nlp are backend-agnostic and always compiled. OPENCV_SOURCES is the
+# explicit, extensible list of opencv-dependent sources: when multimodal LLM
+# lands in nlp, its vision-encoder sources should be appended here too.
+file(GLOB CORE_SOURCES ${CPP_DIR}/core/*.cpp)
+file(GLOB MATH_SOURCES ${CPP_DIR}/extensions/math/*.cpp)
+file(GLOB NLP_SOURCES ${CPP_DIR}/extensions/nlp/*.cpp)
+file(GLOB OPENCV_SOURCES ${CPP_DIR}/extensions/cv/*.cpp)
-add_library(${CMAKE_PROJECT_NAME} SHARED
- ../cpp/RnExecutorch.cpp
+set(RNE_SOURCES
+ ${CPP_DIR}/RnExecutorch.cpp
${CORE_SOURCES}
- ${CV_SOURCES}
${MATH_SOURCES}
${NLP_SOURCES}
cpp-adapter.cpp
)
+if(RNE_ENABLE_OPENCV)
+ list(APPEND RNE_SOURCES ${OPENCV_SOURCES})
+endif()
+
+add_library(${CMAKE_PROJECT_NAME} SHARED ${RNE_SOURCES})
+
target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE
C10_USING_CUSTOM_GENERATED_MACROS=1
EXECUTORCH_ENABLE_EXECUTION_PROFILING=1
)
+# Propagate feature flags as preprocessor defines so C++ can guard includes/calls.
+if(RNE_ENABLE_OPENCV)
+ target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE RNE_ENABLE_OPENCV)
+endif()
+if(RNE_ENABLE_PHONEMIS)
+ target_compile_definitions(${CMAKE_PROJECT_NAME} PRIVATE RNE_ENABLE_PHONEMIS)
+endif()
+
target_include_directories(${CMAKE_PROJECT_NAME} PRIVATE
- ../cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include
- ${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/include
- ${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/third-party/json/include
- ${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/third-party/re2
- ${CMAKE_CURRENT_SOURCE_DIR}/../third-party/include/executorch/extension/llm/tokenizers/third-party/abseil-cpp
+ ${CPP_DIR}
+ ${INCLUDE_DIR}
+ ${INCLUDE_DIR}/executorch/extension/llm/tokenizers/include
+ ${INCLUDE_DIR}/executorch/extension/llm/tokenizers/third-party/json/include
+ ${INCLUDE_DIR}/executorch/extension/llm/tokenizers/third-party/re2
+ ${INCLUDE_DIR}/executorch/extension/llm/tokenizers/third-party/abseil-cpp
)
-# Ensure executorch is linked with WHOLE_ARCHIVE so all static initializers are included
-target_link_options(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--whole-archive)
-target_link_libraries(${CMAKE_PROJECT_NAME} executorch)
-target_link_options(${CMAKE_PROJECT_NAME} PRIVATE -Wl,--no-whole-archive)
+# ------- ExecuTorch core (always required) -------
+add_library(executorch SHARED IMPORTED)
+set_target_properties(executorch PROPERTIES
+ IMPORTED_LOCATION "${LIBS_DIR}/executorch/${ANDROID_ABI}/libexecutorch.so")
+
+# ------- XNNPACK backend (optional) -------
+# Backends ship as separate .so files. When enabled, CMake imports the .so and
+# links libreact-native-executorch.so against it; Gradle packages the .so into
+# the APK (via jniLibs srcDir) and the dynamic linker loads it whenever
+# libreact-native-executorch.so loads. Each backend .so's load-time constructor
+# registers itself with libexecutorch.so's backend registry.
+if(RNE_ENABLE_XNNPACK)
+ add_library(xnnpack_executorch_backend SHARED IMPORTED)
+ set_target_properties(xnnpack_executorch_backend PROPERTIES
+ IMPORTED_LOCATION "${LIBS_DIR}/executorch/${ANDROID_ABI}/libxnnpack_executorch_backend.so")
+endif()
+
+# ------- Vulkan backend (optional) -------
+if(RNE_ENABLE_VULKAN)
+ add_library(vulkan_executorch_backend SHARED IMPORTED)
+ set_target_properties(vulkan_executorch_backend PROPERTIES
+ IMPORTED_LOCATION "${LIBS_DIR}/executorch/${ANDROID_ABI}/libvulkan_executorch_backend.so")
+endif()
+
+# ------- OpenCV (optional, static) -------
+set(OPENCV_LINK_LIBS "")
+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"
+ )
+ 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()
+
+# libexecutorch.so references OpenMP runtime symbols (optimized kernels). Link
+# the static OpenMP runtime into libRnExecutorch.so to resolve them.
+target_link_options(${CMAKE_PROJECT_NAME} PRIVATE -fopenmp -static-openmp)
target_link_libraries(${CMAKE_PROJECT_NAME}
ReactAndroid::jsi
log
- opencv
+ ${OPENCV_LINK_LIBS}
+ executorch
+ z
)
+
+# Linking against the backend .so (when enabled) makes Gradle bundle them and
+# instructs the dynamic linker to load them with libreact-native-executorch.so.
+if(RNE_ENABLE_XNNPACK)
+ target_link_libraries(${CMAKE_PROJECT_NAME} xnnpack_executorch_backend)
+endif()
+if(RNE_ENABLE_VULKAN)
+ target_link_libraries(${CMAKE_PROJECT_NAME} vulkan_executorch_backend)
+endif()
diff --git a/packages/react-native-executorch/android/build.gradle.kts b/packages/react-native-executorch/android/build.gradle.kts
index 57162a0245..17b06d31d0 100644
--- a/packages/react-native-executorch/android/build.gradle.kts
+++ b/packages/react-native-executorch/android/build.gradle.kts
@@ -1,3 +1,5 @@
+import groovy.json.JsonSlurper
+
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
@@ -5,7 +7,7 @@ plugins {
}
/**
- * Helper function to get variables from the root project (App level)
+ * Helper function to get variables from the root project (App level)
* or fall back to local project properties.
*/
fun getExtOrDefault(name: String, default: Any): Any {
@@ -18,6 +20,43 @@ fun getExtOrDefault(name: String, default: Any): Any {
}
}
+/**
+ * Reads the build config written by the postinstall script
+ * (scripts/download-libs.js). Falls back to enabling everything if the file is
+ * missing (e.g. native libs provisioned manually, or CI with pre-cached libs).
+ */
+fun rneBuildConfig(): Map<*, *> {
+ val defaults = mapOf(
+ "enableOpencv" to true,
+ "enablePhonemis" to true,
+ "enableXnnpack" to true,
+ "enableVulkan" to true
+ )
+ val configFile = file("../rne-build-config.json")
+ if (!configFile.exists()) return defaults
+ return try {
+ JsonSlurper().parse(configFile) as Map<*, *>
+ } catch (e: Exception) {
+ logger.warn("[RnExecutorch] Failed to parse rne-build-config.json: ${e.message}. Defaulting to all features enabled.")
+ defaults
+ }
+}
+
+val rneConfig = rneBuildConfig()
+fun rneFlag(key: String): String = if (rneConfig[key] != false) "ON" else "OFF"
+
+/**
+ * ExecuTorch only supports these ABIs. Honor the app's `reactNativeArchitectures`
+ * (e.g. Expo passes `-PreactNativeArchitectures=arm64-v8a` for device builds) so
+ * we only build/provision the ABIs the app actually needs; default to both.
+ */
+fun reactNativeArchitectures(): List {
+ val supported = listOf("arm64-v8a", "x86_64")
+ val value = rootProject.findProperty("reactNativeArchitectures") as String?
+ return value?.split(",")?.map { it.trim() }?.filter { it in supported }
+ ?.takeIf { it.isNotEmpty() } ?: supported
+}
+
android {
namespace = "com.swmansion.rnexecutorch"
compileSdk = (getExtOrDefault("compileSdkVersion", 34) as Number).toInt()
@@ -29,11 +68,16 @@ android {
externalNativeBuild {
cmake {
cppFlags("-fexceptions", "-frtti", "-std=c++20", "-Wall")
- arguments("-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON")
-
- // CRITICAL: Only include ABIs you actually have binaries for
- // Since you only have arm64-v8a in third-party, we pin it here.
- abiFilters.add("arm64-v8a")
+ arguments(
+ "-DANDROID_STL=c++_shared",
+ "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON",
+ "-DRNE_ENABLE_OPENCV=${rneFlag("enableOpencv")}",
+ "-DRNE_ENABLE_PHONEMIS=${rneFlag("enablePhonemis")}",
+ "-DRNE_ENABLE_XNNPACK=${rneFlag("enableXnnpack")}",
+ "-DRNE_ENABLE_VULKAN=${rneFlag("enableVulkan")}"
+ )
+
+ abiFilters.addAll(reactNativeArchitectures())
}
}
}
@@ -46,8 +90,11 @@ android {
sourceSets {
getByName("main") {
- // Pointing to your centralized third-party binaries
- jniLibs.srcDirs("../third-party/android/jniLibs")
+ // Prebuilt executorch + backend .so live under
+ // third-party/android/libs/executorch//. Pointing jniLibs here
+ // makes Gradle package them (libexecutorch.so + the enabled
+ // lib*_executorch_backend.so) into the APK alongside our own .so.
+ jniLibs.srcDirs("../third-party/android/libs/executorch")
// Include generated codegen if using TurboModules
java.srcDirs("${project.buildDir}/generated/source/codegen/java")
}
@@ -82,7 +129,9 @@ dependencies {
// React Native Android Engine
implementation("com.facebook.react:react-android")
- // The ExecuTorch Java/Kotlin wrapper from your third-party folder
+ // The ExecuTorch Java API (ABI-independent). Downloaded (not committed) β it
+ // rides in the core-android-arm64-v8a artifact, extracted by download-libs.js
+ // to third-party/android/libs/executorch.jar.
implementation(files("../third-party/android/libs/executorch.jar"))
// Recommended for modern Kotlin Android development
@@ -94,4 +143,4 @@ extensions.configure("react") {
jsRootDir = file("../src/")
libraryName = "RnExecutorch"
codegenJavaPackageName = "com.swmansion.rnexecutorch"
-}
\ No newline at end of file
+}
diff --git a/packages/react-native-executorch/cpp/RnExecutorch.cpp b/packages/react-native-executorch/cpp/RnExecutorch.cpp
index 3b71b61775..2c094e8fa1 100644
--- a/packages/react-native-executorch/cpp/RnExecutorch.cpp
+++ b/packages/react-native-executorch/cpp/RnExecutorch.cpp
@@ -1,10 +1,13 @@
#include "RnExecutorch.h"
#include "core/install.h"
-#include "extensions/cv/install.h"
#include "extensions/math/install.h"
#include "extensions/nlp/install.h"
+#ifdef RNE_ENABLE_OPENCV
+#include "extensions/cv/install.h"
+#endif
+
using namespace facebook;
namespace rnexecutorch {
@@ -12,7 +15,9 @@ void install(jsi::Runtime &jsiRuntime) {
jsi::Object module = jsi::Object(jsiRuntime);
rnexecutorch::core::install(jsiRuntime, module);
+#ifdef RNE_ENABLE_OPENCV
rnexecutorch::extensions::cv::install(jsiRuntime, module);
+#endif
rnexecutorch::extensions::math::install(jsiRuntime, module);
rnexecutorch::extensions::nlp::install(jsiRuntime, module);
diff --git a/packages/react-native-executorch/package.json b/packages/react-native-executorch/package.json
index d29d74882e..9ffffcda13 100644
--- a/packages/react-native-executorch/package.json
+++ b/packages/react-native-executorch/package.json
@@ -20,8 +20,19 @@
"android",
"ios",
"cpp",
- "third-party",
+ "scripts",
"*.podspec",
+ "third-party",
+ "!third-party/include",
+ "!third-party/android/jniLibs",
+ "!third-party/android/libs",
+ "!third-party/ios/Frameworks",
+ "!third-party/ios/ExecutorchLib",
+ "!third-party/ios/ExecutorchLib.xcframework",
+ "!third-party/ios/XnnpackBackend.xcframework",
+ "!third-party/ios/CoreMLBackend.xcframework",
+ "!third-party/ios/MLXBackend.xcframework",
+ "!third-party/ios/libs",
"!android/build",
"!ios/build",
"!**/__tests__",
@@ -36,7 +47,7 @@
"lint": "eslint \"**/*.{js,ts,tsx}\"",
"prepack": "cp ../../README.md ./README.md",
"postpack": "rm ./README.md",
- "postinstall": "yarn run -T patch-package --patch-dir ../../patches"
+ "postinstall": "node scripts/download-libs.js && yarn run -T patch-package --patch-dir ../../patches"
},
"keywords": [
"text-to-speech",
diff --git a/packages/react-native-executorch/react-native-executorch.podspec b/packages/react-native-executorch/react-native-executorch.podspec
index 5dc748a1ed..62cbb651be 100644
--- a/packages/react-native-executorch/react-native-executorch.podspec
+++ b/packages/react-native-executorch/react-native-executorch.podspec
@@ -2,6 +2,25 @@ require "json"
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
+# Read the build config written by the postinstall script (scripts/download-libs.js).
+# Falls back to all features enabled if the file doesn't exist (e.g. a fresh
+# checkout where the native libs were provisioned manually).
+rne_build_config_path = File.join(__dir__, "rne-build-config.json")
+if File.exist?(rne_build_config_path)
+ rne_build_config = JSON.parse(File.read(rne_build_config_path))
+ enable_opencv = rne_build_config["enableOpencv"] != false
+ enable_phonemis = rne_build_config["enablePhonemis"] != false
+ enable_xnnpack = rne_build_config["enableXnnpack"] != false
+ enable_coreml = rne_build_config["enableCoreml"] != false
+ enable_mlx = rne_build_config["enableMlx"] != false
+else
+ enable_opencv = true
+ enable_phonemis = true
+ enable_xnnpack = true
+ enable_coreml = true
+ enable_mlx = true
+end
+
Pod::Spec.new do |s|
s.name = "react-native-executorch"
s.version = package["version"]
@@ -10,37 +29,132 @@ Pod::Spec.new do |s|
s.license = package["license"]
s.authors = package["author"]
- s.platforms = { :ios => min_ios_version_supported }
+ # ExecuTorch (and the MLX backend) require iOS 17.
+ s.platforms = { :ios => '17.0' }
s.source = { :git => "https://github.com/software-mansion/react-native-executorch.git", :tag => "#{s.version}" }
- s.source_files = "ios/**/*.{h,m,mm,swift,cpp}", "cpp/**/*.{hpp,cpp,c,h}"
- s.private_header_files = "ios/**/*.h"
+ # libthreadpool_*.a ships pthreadpool (incl. the v2 API libXnnpackBackend
+ # depends on) plus cpuinfo in one archive. We link it directly here because
+ # ExecutorchLib.framework keeps those symbols local-only (not exported), so
+ # split-out backend xcframeworks can't resolve them through the framework.
+ # Use the literal Xcode build variable so it is expanded at build time (matching
+ # the HEADER_SEARCH_PATHS below). Do NOT wrap in File.expand_path: that resolves
+ # `$(PODS_TARGET_SRCROOT)` as a literal directory relative to __dir__, baking a
+ # malformed `/$(PODS_TARGET_SRCROOT)/...` path into the linker flags.
+ executorch_binaries_path = '$(PODS_TARGET_SRCROOT)/third-party/ios/libs/executorch'
+
+ # --- Sources ---
+ # OpenCV-dependent sources live under the cv extension. When more tasks land
+ # that need opencv (e.g. the multimodal-LLM vision encoder under nlp), add
+ # their paths to this list so they are excluded together when opencv is off.
+ opencv_source_files = [
+ "cpp/extensions/cv/**/*.{cpp,c,h,hpp}",
+ ]
+
+ s.source_files = [
+ "ios/**/*.{h,m,mm}",
+ "cpp/**/*.{cpp,c,h,hpp}",
+ ]
+
+ exclude_files = []
+ exclude_files += opencv_source_files unless enable_opencv
+ s.exclude_files = exclude_files
+
+ # --- Preprocessor flags ---
+ # phonemis is wired for forward-compat (the TTS task is not yet ported to the
+ # rewrite, so no source compiles against it today).
+ extra_compiler_flags = []
+ extra_compiler_flags << "-DRNE_ENABLE_OPENCV" if enable_opencv
+ extra_compiler_flags << "-DRNE_ENABLE_PHONEMIS" if enable_phonemis
+ extra_compiler_flags << "-DRNE_ENABLE_XNNPACK" if enable_xnnpack
+ extra_compiler_flags << "-DRNE_ENABLE_COREML" if enable_coreml
+ extra_compiler_flags << "-DRNE_ENABLE_MLX" if enable_mlx
+
+ # --- Link flags ---
+ physical_ldflags = [
+ '$(inherited)',
+ "\"#{executorch_binaries_path}/libthreadpool_ios.a\"",
+ ]
+ simulator_ldflags = [
+ '$(inherited)',
+ "\"#{executorch_binaries_path}/libthreadpool_simulator.a\"",
+ ]
+
+ xnnpack_xcframework_path = '$(PODS_TARGET_SRCROOT)/third-party/ios/XnnpackBackend.xcframework'
+ coreml_xcframework_path = '$(PODS_TARGET_SRCROOT)/third-party/ios/CoreMLBackend.xcframework'
+ mlx_xcframework_path = '$(PODS_TARGET_SRCROOT)/third-party/ios/MLXBackend.xcframework'
+
+ if enable_xnnpack
+ physical_ldflags << "-force_load \"#{xnnpack_xcframework_path}/ios-arm64/libXnnpackBackend.a\""
+ simulator_ldflags << "-force_load \"#{xnnpack_xcframework_path}/ios-arm64-simulator/libXnnpackBackend.a\""
+ end
+
+ if enable_coreml
+ physical_ldflags << "-force_load \"#{coreml_xcframework_path}/ios-arm64/libCoreMLBackend.a\""
+ simulator_ldflags << "-force_load \"#{coreml_xcframework_path}/ios-arm64-simulator/libCoreMLBackend.a\""
+ end
- s.ios.vendored_frameworks = "third-party/ios/Frameworks/ExecutorchLib.xcframework", "third-party/ios/Frameworks/opencv2.xcframework"
- s.frameworks = "CoreML", "Metal", "MetalPerformanceShaders", "Accelerate"
- s.library = "sqlite3"
+ # MLX backend uses Metal APIs (`MTLTensorDomain`, `MTLIOErrorDomain`) that ship
+ # in iPhoneOS.sdk but NOT iPhoneSimulator.sdk, and the iOS simulator can't drive
+ # MLX-on-Metal anyway. MLX ships the device slice only β link it on device only.
+ if enable_mlx
+ physical_ldflags << "-force_load \"#{mlx_xcframework_path}/ios-arm64/libMLXBackend.a\""
+ end
+
+ s.user_target_xcconfig = {
+ "OTHER_LDFLAGS[sdk=iphoneos*]" => physical_ldflags.join(' '),
+ "OTHER_LDFLAGS[sdk=iphonesimulator*]" => simulator_ldflags.join(' '),
+ 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64',
+ }
s.pod_target_xcconfig = {
+ "USE_HEADERMAP" => "YES",
"CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
-
+ "OTHER_CPLUSPLUSFLAGS" => extra_compiler_flags.join(' '),
"GCC_PREPROCESSOR_DEFINITIONS" => [
"$(inherited)",
"C10_USING_CUSTOM_GENERATED_MACROS=1",
"EXECUTORCH_ENABLE_EXECUTION_PROFILING=1",
].join(' '),
-
"HEADER_SEARCH_PATHS" => [
"\"$(PODS_TARGET_SRCROOT)/cpp\"",
"\"$(PODS_TARGET_SRCROOT)/third-party/include\"",
+ "\"$(PODS_TARGET_SRCROOT)/third-party/include/cpuinfo\"",
+ "\"$(PODS_TARGET_SRCROOT)/third-party/include/pthreadpool\"",
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/include\"",
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/third-party/json/include\"",
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/third-party/re2\"",
"\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/third-party/abseil-cpp\"",
].join(' '),
-
- "WARNING_CFLAGS" => "-Wno-documentation"
+ "WARNING_CFLAGS" => "-Wno-documentation",
+ 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64',
}
+ libs = ["z"]
+ libs << "sqlite3" if enable_coreml
+ s.libraries = libs
+
+ system_frameworks = ["Accelerate"]
+ system_frameworks << "CoreML" if enable_coreml
+ # MLX needs Metal at runtime; the GPU kernels in mlx.metallib are compiled
+ # against the Metal toolchain.
+ system_frameworks += ["Metal", "MetalKit", "MetalPerformanceShaders"] if enable_mlx
+ s.frameworks = system_frameworks
+
+ # MLX runtime resolves its compiled GPU kernels via dladdr on a function symbol
+ # from libMLXBackend.a, then loads `mlx.metallib` from the same directory as
+ # that symbol's host binary. Because libMLXBackend.a is force-loaded into the
+ # app's main executable, the metallib has to land in the app bundle's main
+ # resource path. `s.ios.resource` achieves that via CocoaPods' resource copy.
+ s.ios.resource = "third-party/ios/libs/executorch/mlx.metallib" if enable_mlx
+
+ # Backend xcframeworks are linked via force_load in OTHER_LDFLAGS (needed to
+ # preserve __attribute__((constructor)) backend registrations). Only
+ # ExecutorchLib goes in vendored_frameworks to avoid duplicate symbol errors.
+ s.ios.vendored_frameworks = ["third-party/ios/ExecutorchLib.xcframework"]
+
+ # iOS OpenCV is provided by the opencv-rne CocoaPod (not a downloaded tarball).
+ 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..608fe82418
--- /dev/null
+++ b/packages/react-native-executorch/scripts/download-libs.js
@@ -0,0 +1,449 @@
+/**
+ * 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):
+ *
+ * headers.tar.gz -- ExecuTorch + c10 + torch + tokenizers + opencv
+ * headers (platform-independent; always downloaded)
+ * 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 (without xnnpack/coreml)
+ * 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
+ * xnnpack-android-arm64-v8a.tar.gz -- libxnnpack_executorch_backend.so (Android)
+ * xnnpack-android-x86_64.tar.gz
+ * xnnpack-ios.tar.gz -- XnnpackBackend.xcframework (iOS)
+ * vulkan-android-arm64-v8a.tar.gz -- libvulkan_executorch_backend.so (Android only)
+ * vulkan-android-x86_64.tar.gz
+ * coreml-ios.tar.gz -- CoreMLBackend.xcframework (iOS only)
+ * mlx-ios.tar.gz -- MLXBackend.xcframework + mlx.metallib (iOS only)
+ *
+ * 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) β three optional arrays, all merged into a single set:
+ * "react-native-executorch": {
+ * "backends": ["xnnpack", "coreml", "mlx", "vulkan"],
+ * "libs": ["opencv", "phonemis"],
+ * "features": ["llm", "textToSpeech", "objectDetection"]
+ * }
+ *
+ * `features` is sugar β each one expands to a set of backends + libs via FEATURE_MAP below.
+ * If no `react-native-executorch` block is present, every backend and lib defaults to ON.
+ *
+ * Recognized values:
+ * backends: xnnpack, coreml (iOS), mlx (iOS), vulkan (Android)
+ * libs: opencv, phonemis
+ * features: llm, multimodalLLM, speechToText, textToSpeech, vad, privacyFilter,
+ * textEmbeddings, imageEmbeddings,
+ * classification, objectDetection, semanticSegmentation, instanceSegmentation,
+ * ocr, verticalOCR, keypointDetection, styleTransfer, textToImage, segmentAnything,
+ * tokenizer
+ *
+ * Platform applicability:
+ * opencv Android + iOS β downloaded artifact
+ * phonemis Android + iOS β built from in-tree source at third-party/common/phonemis (git submodule);
+ * this toggle only gates compilation (RNE_ENABLE_PHONEMIS), no download.
+ * xnnpack Android (libxnnpack_executorch_backend.so) + iOS (XnnpackBackend.xcframework)
+ * coreml iOS only β toggles CoreMLBackend.xcframework.
+ * mlx iOS only β toggles MLXBackend.xcframework + mlx.metallib resource.
+ * vulkan Android only β toggles libvulkan_executorch_backend.so.
+ *
+ * Environment variables:
+ * RNET_SKIP_DOWNLOAD=1 -- skip download entirely (for CI with pre-cached libs)
+ * RNET_HEADERS_ONLY=1 -- fetch only headers.tar.gz, no native libs
+ * (e.g. clang-tidy / IDE tooling needing include paths)
+ * RNET_NO_X86_64=1 -- skip the android-x86_64 (emulator) ABI
+ * 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 -----------------------------------------------------------
+
+const ALL_BACKENDS = ['xnnpack', 'coreml', 'mlx', 'vulkan'];
+const ALL_LIBS = ['opencv', 'phonemis'];
+
+// features -> { backends, libs }
+// Backend lists are the union of what at least one model in that family ships
+// today (per src/models.ts). When a new variant lands for a model that adds
+// e.g. coreml or vulkan support, bump the family here.
+const FEATURE_MAP = {
+ // Text-only LLMs ship xnnpack + mlx (Gemma 4 ships an MLX iOS export).
+ llm: { backends: ['xnnpack', 'mlx'], libs: [] },
+ // Multimodal LLMs add vulkan (Gemma-3-multimodal ships a Vulkan export) and
+ // mlx (Gemma 4 ships an MLX iOS export); the vision encoder needs opencv.
+ multimodalLLM: { backends: ['xnnpack', 'mlx', 'vulkan'], libs: ['opencv'] },
+ // Privacy filter classifiers β xnnpack only.
+ privacyFilter: { backends: ['xnnpack'], libs: [] },
+ // Whisper ships xnnpack + coreml.
+ speechToText: { backends: ['xnnpack', 'coreml'], libs: [] },
+ // Kokoro ships xnnpack only.
+ textToSpeech: { backends: ['xnnpack'], libs: ['phonemis'] },
+ // FSMN VAD β xnnpack only.
+ vad: { backends: ['xnnpack'], libs: [] },
+ textEmbeddings: { backends: ['xnnpack'], libs: [] },
+ imageEmbeddings: { backends: ['xnnpack'], libs: ['opencv'] },
+ // EfficientNet ships xnnpack + coreml.
+ classification: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
+ // YOLO is xnnpack-only, ssdlite/rf_detr add coreml β union.
+ objectDetection: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
+ // Keypoint detection (#1280): BlazeFace + YOLO26-pose ship xnnpack; RF-DETR
+ // keypoint adds coreml + mlx β union. (Named to track the useKeypointDetector
+ // hook; main calls this poseEstimation.)
+ keypointDetection: { backends: ['xnnpack', 'coreml', 'mlx'], libs: ['opencv'] },
+ // DeepLab/FCN/LR-ASPP/selfie β xnnpack only.
+ semanticSegmentation: { backends: ['xnnpack'], libs: ['opencv'] },
+ // YOLO-seg xnnpack-only, rf_detr-seg/fastsam add coreml β union.
+ instanceSegmentation: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
+ // CRAFT + CRNN β xnnpack only.
+ ocr: { backends: ['xnnpack'], libs: ['opencv'] },
+ verticalOCR: { backends: ['xnnpack'], libs: ['opencv'] },
+ // All style-transfer presets ship xnnpack + coreml.
+ styleTransfer: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
+ // BK-SDM β xnnpack only.
+ textToImage: { backends: ['xnnpack'], libs: ['opencv'] },
+ // FastSAM ships xnnpack + coreml.
+ segmentAnything: { backends: ['xnnpack', 'coreml'], libs: ['opencv'] },
+ // Tokenizer is pure-CPU string ops resolved from libexecutorch; needs no
+ // backend or extra lib. Listed so a tokenizer-only app can opt into core only.
+ tokenizer: { backends: [], libs: [] },
+};
+
+function readUserConfig() {
+ const allOn = () => ({ backends: [...ALL_BACKENDS], libs: [...ALL_LIBS] });
+
+ // 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 backends + libs.'
+ );
+ return allOn();
+ }
+
+ let rneConfig;
+ try {
+ const userPackageJson = JSON.parse(
+ fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
+ );
+ rneConfig = userPackageJson['react-native-executorch'];
+ } catch {
+ console.warn(
+ '[react-native-executorch] Could not read app package.json, enabling all backends + libs.'
+ );
+ return allOn();
+ }
+
+ if (rneConfig === undefined) return allOn();
+
+ if (rneConfig.extras !== undefined) {
+ throw new Error(
+ '[react-native-executorch] The legacy `extras` field is no longer supported. ' +
+ 'Use `backends`, `libs`, and/or `features` instead.'
+ );
+ }
+
+ const backends = new Set(rneConfig.backends ?? []);
+ const libs = new Set(rneConfig.libs ?? []);
+
+ for (const feature of rneConfig.features ?? []) {
+ const expansion = FEATURE_MAP[feature];
+ if (!expansion) {
+ const known = Object.keys(FEATURE_MAP).join(', ');
+ throw new Error(
+ `[react-native-executorch] Unknown feature "${feature}". Known features: ${known}.`
+ );
+ }
+ expansion.backends.forEach((b) => backends.add(b));
+ expansion.libs.forEach((l) => libs.add(l));
+ }
+
+ for (const b of backends) {
+ if (!ALL_BACKENDS.includes(b)) {
+ throw new Error(
+ `[react-native-executorch] Unknown backend "${b}". Known: ${ALL_BACKENDS.join(', ')}.`
+ );
+ }
+ }
+ for (const l of libs) {
+ if (!ALL_LIBS.includes(l)) {
+ throw new Error(
+ `[react-native-executorch] Unknown lib "${l}". Known: ${ALL_LIBS.join(', ')}.`
+ );
+ }
+ }
+
+ return { backends: [...backends], libs: [...libs] };
+}
+
+function writeBuildConfig({ backends, libs }) {
+ const config = {
+ enableOpencv: libs.includes('opencv'),
+ enablePhonemis: libs.includes('phonemis'),
+ enableXnnpack: backends.includes('xnnpack'),
+ enableCoreml: backends.includes('coreml'),
+ enableMlx: backends.includes('mlx'),
+ enableVulkan: backends.includes('vulkan'),
+ };
+ fs.writeFileSync(
+ path.join(PACKAGE_ROOT, 'rne-build-config.json'),
+ JSON.stringify(config, null, 2)
+ );
+ return config;
+}
+
+// Warn when a backend is opted in but the build targets only the platform where
+// it has no effect (coreml/mlx=iOS-only, vulkan=Android-only). Surfacing it at
+// install time is friendlier than the user wondering why an opt-out had no effect.
+function warnAboutPlatformAsymmetry({ backends }, targets) {
+ const hasAndroid = targets.some((t) => t.startsWith('android'));
+ const hasIos = targets.includes('ios');
+ if (hasAndroid && !hasIos && backends.includes('coreml')) {
+ console.warn(
+ '[react-native-executorch] coreml is enabled but the build targets only Android; CoreML is iOS-only and the flag has no effect here.'
+ );
+ }
+ if (hasAndroid && !hasIos && backends.includes('mlx')) {
+ console.warn(
+ '[react-native-executorch] mlx is enabled but the build targets only Android; the MLX backend is iOS-only and the flag has no effect here.'
+ );
+ }
+ if (hasIos && !hasAndroid && backends.includes('vulkan')) {
+ console.warn(
+ '[react-native-executorch] vulkan is enabled but the build targets only iOS; the Vulkan backend is Android-only and the flag has no effect here.'
+ );
+ }
+}
+
+// ---- 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 backend / lib is enabled.
+function getArtifacts(targets, { backends, libs }) {
+ const artifacts = [];
+
+ // Headers (ExecuTorch + c10 + torch + tokenizers + opencv) are
+ // platform-independent and always needed at build time. The tarball contains
+ // an `include/` dir, so it extracts to third-party/include/.
+ artifacts.push(makeArtifact('headers', THIRD_PARTY_DIR));
+
+ // RNET_HEADERS_ONLY provisions just the platform-independent headers (no
+ // per-target native libs) β e.g. for clang-tidy / IDE tooling that needs the
+ // include paths but never links or runs the binaries.
+ if (process.env.RNET_HEADERS_ONLY) {
+ return 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 (libs.includes('opencv') && target !== 'ios') {
+ artifacts.push(makeArtifact(`opencv-${target}`, destDir));
+ }
+
+ // phonemis is built from in-tree source (third-party/common/phonemis submodule);
+ // no artifact download required.
+
+ if (backends.includes('xnnpack')) {
+ artifacts.push(makeArtifact(`xnnpack-${target}`, destDir));
+ }
+
+ // CoreML is iOS only
+ if (backends.includes('coreml') && target === 'ios') {
+ artifacts.push(makeArtifact(`coreml-${target}`, destDir));
+ }
+
+ // MLX is iOS only
+ if (backends.includes('mlx') && target === 'ios') {
+ artifacts.push(makeArtifact(`mlx-${target}`, destDir));
+ }
+
+ // Vulkan is Android only
+ if (backends.includes('vulkan') && target.startsWith('android')) {
+ artifacts.push(makeArtifact(`vulkan-${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 = (currentUrl) => {
+ const client = currentUrl.startsWith('http://') ? http : https;
+ const headers = {};
+ if (process.env.GITHUB_TOKEN && currentUrl.includes('github.com')) {
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
+ }
+ client.get(currentUrl, { 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 ${currentUrl}`));
+ }
+ 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's enabled
+ const config = readUserConfig();
+ writeBuildConfig(config);
+ return;
+ }
+
+ const config = readUserConfig();
+ const buildConfig = writeBuildConfig(config);
+ console.log(
+ `[react-native-executorch] Backends: [${config.backends.join(', ') || 'β'}]; Libs: [${config.libs.join(', ') || 'β'}]`
+ );
+ console.log(
+ `[react-native-executorch] Build flags: opencv=${buildConfig.enableOpencv}, phonemis=${buildConfig.enablePhonemis}, xnnpack=${buildConfig.enableXnnpack}, coreml=${buildConfig.enableCoreml}, mlx=${buildConfig.enableMlx}, vulkan=${buildConfig.enableVulkan}`
+ );
+
+ const targets = detectTargets();
+ warnAboutPlatformAsymmetry(config, targets);
+ const artifacts = getArtifacts(targets, config);
+
+ 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..a89dd19f12
--- /dev/null
+++ b/packages/react-native-executorch/scripts/package-release-artifacts.sh
@@ -0,0 +1,259 @@
+#!/usr/bin/env bash
+# package-release-artifacts.sh
+#
+# Packages the locally-staged 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
+# xnnpack-android-arm64-v8a.tar.gz + .sha256
+# xnnpack-android-x86_64.tar.gz + .sha256
+# vulkan-android-arm64-v8a.tar.gz + .sha256
+# vulkan-android-x86_64.tar.gz + .sha256
+# core-ios.tar.gz + .sha256
+# xnnpack-ios.tar.gz + .sha256
+# coreml-ios.tar.gz + .sha256
+# mlx-ios.tar.gz + .sha256 (device-slice xcframework + mlx.metallib resource)
+#
+# Note: phonemis ships as in-tree source (third-party/common/phonemis submodule),
+# not as a tarball.
+#
+# Note: iOS OpenCV is provided via CocoaPods (opencv-rne), not a tarball.
+#
+# Note: MLX ships the iOS device slice only β the iOS simulator cannot drive
+# MLX-on-Metal, so no simulator slice is built or packaged.
+#
+# 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"
+
+INCLUDE_DIR="$PACKAGE_ROOT/third-party/include"
+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 a single file into a tarball, placing it at the given relative path.
+# package_file
+package_file() {
+ local name=$1
+ local rel_path=$2 # directory path inside the tarball
+ local src_file=$3 # full path to the source file
+ local out_file="$OUT/$name.tar.gz"
+ local tmp
+ tmp=$(mktemp -d)
+
+ echo " β $name"
+
+ if [ ! -f "$src_file" ]; then
+ echo " β Source file not found: $src_file" >&2
+ rm -rf "$tmp"
+ exit 1
+ fi
+
+ mkdir -p "$tmp/$rel_path"
+ cp "$src_file" "$tmp/$rel_path/"
+
+ 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"
+}
+
+# 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"
+}
+
+# ---- Headers ----------------------------------------------------------------
+# Platform-independent ExecuTorch + c10 + torch + tokenizers + opencv headers.
+# Staged under include/ so the tarball extracts to third-party/include/.
+#
+# third-party/include/ must first be assembled by scripts/vendor-headers.sh
+# (the executorch header surface spans the source tree, the build-generated
+# headers, the xcframework c10/torch, and opencv β a copy of the install tree
+# alone is incomplete, e.g. it omits extension/llm/{runner,custom_ops}).
+
+if [ ! -d "$INCLUDE_DIR/executorch/extension/llm/runner" ]; then
+ echo " β $INCLUDE_DIR looks incomplete (no extension/llm/runner)." >&2
+ echo " Run scripts/vendor-headers.sh first." >&2
+ exit 1
+fi
+
+echo ""
+echo "Headers:"
+
+package_merged "headers" \
+ "include" "$INCLUDE_DIR"
+
+# ---- Android ----------------------------------------------------------------
+
+echo ""
+echo "Android:"
+
+# core-android bundles the per-ABI executorch .so. pthreadpool + cpuinfo are
+# statically linked into libexecutorch.so (not shipped separately). The
+# ABI-independent executorch.jar (ExecuTorch Java API for the JNI bridge) rides
+# along in the arm64 core tarball, which is always downloaded.
+echo " β core-android-arm64-v8a"
+_ca_tmp=$(mktemp -d)
+mkdir -p "$_ca_tmp/executorch/arm64-v8a"
+cp -r "$ANDROID_LIBS/executorch/arm64-v8a/." "$_ca_tmp/executorch/arm64-v8a/"
+cp "$ANDROID_LIBS/executorch.jar" "$_ca_tmp/executorch.jar"
+tar -czf "$OUT/core-android-arm64-v8a.tar.gz" -C "$_ca_tmp" .
+shasum -a 256 "$OUT/core-android-arm64-v8a.tar.gz" | awk '{print $1}' > "$OUT/core-android-arm64-v8a.tar.gz.sha256"
+echo " β $(du -sh "$OUT/core-android-arm64-v8a.tar.gz" | cut -f1)"
+rm -rf "$_ca_tmp"
+
+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"
+
+# phonemis is built from in-tree source (third-party/common/phonemis submodule);
+# no Android tarball is produced.
+
+# XNNPACK and Vulkan each ship as standalone shared libraries (opt-in backends).
+package_file "xnnpack-android-arm64-v8a" \
+ "executorch/arm64-v8a" "$ANDROID_LIBS/executorch/arm64-v8a/libxnnpack_executorch_backend.so"
+
+package_file "xnnpack-android-x86_64" \
+ "executorch/x86_64" "$ANDROID_LIBS/executorch/x86_64/libxnnpack_executorch_backend.so"
+
+package_file "vulkan-android-arm64-v8a" \
+ "executorch/arm64-v8a" "$ANDROID_LIBS/executorch/arm64-v8a/libvulkan_executorch_backend.so"
+
+package_file "vulkan-android-x86_64" \
+ "executorch/x86_64" "$ANDROID_LIBS/executorch/x86_64/libvulkan_executorch_backend.so"
+
+# ---- iOS --------------------------------------------------------------------
+# Note: OpenCV for iOS is provided by CocoaPods (opencv-rne dependency).
+# No opencv-ios tarball is needed.
+
+echo ""
+echo "iOS:"
+
+# pthreadpool + cpuinfo are bundled into libthreadpool_*.a (in libs/executorch),
+# so no separate libs/pthreadpool or libs/cpuinfo dirs are shipped.
+package_merged "core-ios" \
+ "ExecutorchLib.xcframework" "$IOS_DIR/ExecutorchLib.xcframework" \
+ "libs/executorch" "$IOS_DIR/libs/executorch"
+
+# phonemis is built from in-tree source (third-party/common/phonemis submodule);
+# no iOS tarball is produced.
+
+package_merged "xnnpack-ios" \
+ "XnnpackBackend.xcframework" "$IOS_DIR/XnnpackBackend.xcframework"
+
+package_merged "coreml-ios" \
+ "CoreMLBackend.xcframework" "$IOS_DIR/CoreMLBackend.xcframework"
+
+# MLX ships a device-only xcframework plus a separate mlx.metallib that must
+# land in the app bundle at runtime (pod Resource via podspec). The iOS
+# simulator cannot drive MLX-on-Metal, so no simulator slice exists. Stage both
+# into one tarball.
+echo " β mlx-ios"
+_mlx_tmp=$(mktemp -d)
+if [ ! -d "$IOS_DIR/MLXBackend.xcframework" ]; then
+ echo " β Source directory not found: $IOS_DIR/MLXBackend.xcframework" >&2
+ exit 1
+fi
+if [ ! -f "$IOS_DIR/libs/executorch/mlx.metallib" ]; then
+ echo " β Source file not found: $IOS_DIR/libs/executorch/mlx.metallib" >&2
+ exit 1
+fi
+mkdir -p "$_mlx_tmp/MLXBackend.xcframework"
+cp -r "$IOS_DIR/MLXBackend.xcframework/." "$_mlx_tmp/MLXBackend.xcframework/"
+mkdir -p "$_mlx_tmp/libs/executorch"
+cp "$IOS_DIR/libs/executorch/mlx.metallib" "$_mlx_tmp/libs/executorch/"
+tar -czf "$OUT/mlx-ios.tar.gz" -C "$_mlx_tmp" .
+shasum -a 256 "$OUT/mlx-ios.tar.gz" | awk '{print $1}' > "$OUT/mlx-ios.tar.gz.sha256"
+echo " β $(du -sh "$OUT/mlx-ios.tar.gz" | cut -f1)"
+rm -rf "$_mlx_tmp"
+
+# ---- 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/scripts/vendor-headers.sh b/packages/react-native-executorch/scripts/vendor-headers.sh
new file mode 100755
index 0000000000..2c7fa583c6
--- /dev/null
+++ b/packages/react-native-executorch/scripts/vendor-headers.sh
@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+# vendor-headers.sh
+#
+# Assembles the platform-independent C/C++ header tree that ships as the
+# `headers` release artifact (headers.tar.gz) and is consumed by the iOS
+# podspec + android/CMakeLists.txt include paths.
+#
+# The header surface spans FOUR sources (which is why a hand copy of the
+# executorch CMake *install* tree alone is incomplete β it omits the
+# source-only headers such as extension/llm/{runner,custom_ops,apple}):
+#
+# 1. ExecuTorch C++ source headers ($ET/{runtime,extension,...})
+# 2. Build-generated flatbuffer headers ($ET/cmake-out*/**/*_generated.h)
+# 3. pytorch c10 / torch headers (assembled xcframework Headers)
+# 4. XNNPACK/threadpool loose deps + opencv2 (install include root / opencv)
+#
+# Usage:
+# ./scripts/vendor-headers.sh [out-dir]
+#
+# a built software-mansion-labs/executorch@ms/separate-backends
+# checkout (needs cmake-out* dirs for the generated headers
+# and the assembled cmake-out/executorch.xcframework).
+# a directory containing `opencv2/` (the OpenCV prebuilt SDK
+# headers; same source as the opencv-rne pod). OpenCV is not
+# built from executorch.
+# [out-dir] defaults to /third-party/include.
+#
+# Re-run scripts/package-release-artifacts.sh afterwards to pack headers.tar.gz.
+
+set -euo pipefail
+
+if [ "$#" -lt 2 ]; then
+ echo "usage: $0 [out-dir]" >&2
+ exit 1
+fi
+
+ET="$(cd "$1" && pwd)"
+OPENCV_INC="$(cd "$2" && pwd)"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
+OUT="${3:-$PACKAGE_ROOT/third-party/include}"
+
+HDR_INCLUDES=(--include='*/' --include='*.h' --include='*.hpp' --include='*.cuh' --include='*.inc' --exclude='*')
+
+echo "Vendoring headers"
+echo " executorch : $ET"
+echo " opencv : $OPENCV_INC"
+echo " out : $OUT"
+
+rm -rf "$OUT"
+mkdir -p "$OUT/executorch"
+
+# --- 1. ExecuTorch C++ source headers ---------------------------------------
+# The executorch repo root IS the `executorch/` include namespace. These source
+# subtrees carry the full public C++ surface, including the LLM runner helpers
+# (extension/llm/{runner,custom_ops,apple,sampler}) and the bundled tokenizer
+# third-party headers under extension/llm/tokenizers/third-party/.
+echo " [1/4] executorch source headers"
+for sub in runtime extension kernels configurations backends schema devtools; do
+ [ -d "$ET/$sub" ] || continue
+ rsync -am "${HDR_INCLUDES[@]}" "$ET/$sub/" "$OUT/executorch/$sub/"
+done
+
+# --- 2. Build-generated / installed headers ---------------------------------
+# The build emits headers that are NOT in the source tree: flatbuffer
+# *_generated.h plus codegen'd kernel bindings (kernels/*/Functions.h,
+# NativeFunctions.h). Overlay the whole install include/executorch tree on top
+# of the source copy (same-named files are identical; this only adds).
+echo " [2/4] generated + installed executorch headers"
+for inst in "$ET"/cmake-out*/include "$ET"/cmake-out*/schema/include "$ET"/cmake-out*/devtools/include; do
+ [ -d "$inst/executorch" ] || continue
+ rsync -am "${HDR_INCLUDES[@]}" "$inst/executorch/" "$OUT/executorch/"
+done
+
+# --- 3. pytorch c10 / torch headers -----------------------------------------
+# c10 (scalar types etc.) and torch/headeronly are referenced as and
+# . The assembled xcframework Headers carry a clean copy.
+echo " [3/4] c10 / torch headers"
+XCH="$ET/cmake-out/executorch.xcframework/ios-arm64/Headers"
+if [ ! -d "$XCH/c10" ]; then
+ echo " β expected c10 headers at $XCH/c10 (run build_apple_frameworks.sh first)" >&2
+ exit 1
+fi
+rsync -am "${HDR_INCLUDES[@]}" "$XCH/c10/" "$OUT/c10/"
+rsync -am "${HDR_INCLUDES[@]}" "$XCH/torch/" "$OUT/torch/"
+# torch/headeronly is also referenced as a top-level include root.
+[ -d "$XCH/torch/headeronly" ] && rsync -am "${HDR_INCLUDES[@]}" "$XCH/torch/headeronly/" "$OUT/headeronly/"
+
+# --- 4. XNNPACK/threadpool loose deps + opencv2 -----------------------------
+# Backend/tokenizer code includes , , ,
+# , directly. They sit at the install include root, not under
+# executorch/. opencv2 ships from the OpenCV prebuilt (not executorch).
+echo " [4/4] loose deps + opencv2"
+for root in "$ET"/cmake-out*/include; do
+ [ -d "$root" ] || continue
+ for f in cpuinfo.h pthreadpool.h xnnpack.h fxdiv.h pcre2.h pcre2posix.h; do
+ [ -f "$root/$f" ] && cp "$root/$f" "$OUT/"
+ done
+ [ -d "$root/kai" ] && rsync -am "${HDR_INCLUDES[@]}" "$root/kai/" "$OUT/kai/"
+ # The install tree also exposes the tokenizer third-party deps at the include
+ # root (absl/, re2/, nlohmann/). Keep them here too so //
+ # resolve via the root -I, in addition to the nested copies the
+ # tokenizer build references under .../tokenizers/third-party/.
+ for d in absl re2 nlohmann; do
+ [ -d "$root/$d" ] && rsync -am "${HDR_INCLUDES[@]}" "$root/$d/" "$OUT/$d/"
+ done
+done
+if [ ! -d "$OPENCV_INC/opencv2" ]; then
+ echo " β no opencv2/ under $OPENCV_INC" >&2
+ exit 1
+fi
+rsync -am "${HDR_INCLUDES[@]}" "$OPENCV_INC/opencv2/" "$OUT/opencv2/"
+
+echo "Done. $(find "$OUT" -type f | wc -l | tr -d ' ') headers in $OUT"
diff --git a/packages/react-native-executorch/src/hooks/useResourceDownload.ts b/packages/react-native-executorch/src/hooks/useResourceDownload.ts
index 1cb3f3add1..3768910653 100644
--- a/packages/react-native-executorch/src/hooks/useResourceDownload.ts
+++ b/packages/react-native-executorch/src/hooks/useResourceDownload.ts
@@ -21,7 +21,7 @@ const djb2 = (s: string): number => {
* network/disk errors.
* @category Hooks
* @experimental Subject to change once the temporary react-native-fs dependency is replaced.
- * See {@link https://github.com/software-mansion/react-native-executorch/issues/1253 | Issue #1253}.
+ * See [Issue #1253](https://github.com/software-mansion/react-native-executorch/issues/1253).
* @param source The remote URL or local path to the resource. If it's already a
* local path, it is returned immediately as is.
* @param preventLoad If true, prevents checks and downloads, resetting the hook
diff --git a/packages/react-native-executorch/src/utils.ts b/packages/react-native-executorch/src/utils.ts
index c953bd3ace..1c457b3062 100644
--- a/packages/react-native-executorch/src/utils.ts
+++ b/packages/react-native-executorch/src/utils.ts
@@ -22,7 +22,7 @@ export function getRegisteredBackends(): string[] {
* (inputs/outputs shapes, types, and tags), and deletes the temporary file
* before returning.
* @category Utils
- * @experimental Subject to change once the temporary react-native-fs dependency is replaced. See {@link https://github.com/software-mansion/react-native-executorch/issues/1253 | Issue #1253}.
+ * @experimental Subject to change once the temporary react-native-fs dependency is replaced. See [Issue #1253](https://github.com/software-mansion/react-native-executorch/issues/1253).
* @param source The remote HTTP URL or local path to the `.pte` model file.
* @returns A promise resolving to an object containing the model source and
* method signature metadata.
diff --git a/packages/react-native-executorch/third-party/.gitignore b/packages/react-native-executorch/third-party/.gitignore
index 75e91bee9c..63365826b9 100644
--- a/packages/react-native-executorch/third-party/.gitignore
+++ b/packages/react-native-executorch/third-party/.gitignore
@@ -1,7 +1,31 @@
-# Native ExecuTorch artifacts (headers, .so/.jar libs, xcframeworks) are
-# provisioned locally and NOT committed, until split builds land (see README.md).
-# Ignore everything here except this file, the README, and the common/ submodules.
-/*
-!/.gitignore
-!/README.md
-!/common/
+# Downloaded native artifacts are provisioned at postinstall by
+# scripts/download-libs.js (from GitHub Releases) and are NOT committed.
+#
+# Committed here (source, not per-build binaries):
+# - README.md, this .gitignore
+# - common/ (phonemis submodule, built from source)
+# - ios/ExecutorchLib/ (xcodeproj wrapper that builds ExecutorchLib.xcframework)
+
+# --- Headers (ExecuTorch + c10 + tokenizer + opencv) β downloaded, not committed ---
+/include/
+
+# --- Android: downloaded per-ABI binaries ---
+/android/jniLibs/
+/android/libs/executorch/
+# ABI-independent ExecuTorch Java API; rides in the core-android-arm64 tarball.
+/android/libs/executorch.jar
+/android/libs/opencv/
+/android/libs/opencv-third-party/
+/android/libs/pthreadpool/
+/android/libs/cpuinfo/
+
+# --- iOS: downloaded xcframeworks + static archives ---
+/ios/Frameworks/
+/ios/ExecutorchLib.xcframework/
+/ios/XnnpackBackend.xcframework/
+/ios/CoreMLBackend.xcframework/
+/ios/MLXBackend.xcframework/
+/ios/libs/
+
+# Build artifacts produced by scripts/package-release-artifacts.sh
+/dist-artifacts/
diff --git a/packages/react-native-executorch/third-party/README.md b/packages/react-native-executorch/third-party/README.md
index b05f958921..a919852047 100644
--- a/packages/react-native-executorch/third-party/README.md
+++ b/packages/react-native-executorch/third-party/README.md
@@ -1,23 +1,49 @@
# third-party
-Native ExecuTorch binaries and headers are **not** committed to this branch.
+Native ExecuTorch artifacts are split into **committed source** and
+**on-demand binaries**.
-The core package's `android/CMakeLists.txt` and `react-native-executorch.podspec`
-expect ExecuTorch artifacts under this directory:
+## Committed to git
-- `include/` β ExecuTorch + c10 + torch headers, including the `pytorch/tokenizers`
- headers under `include/executorch/extension/llm/tokenizers/include` (used by the
- nlp/tokenizer extension; the `tokenizers::HFTokenizer` symbols resolve from
- `libexecutorch`, which is built with the llm/tokenizers extension)
-- `android/jniLibs//libexecutorch.so`, `android/libs/executorch.jar`
-- `ios/Frameworks/ExecutorchLib.xcframework`
+- `ios/ExecutorchLib/` β the Xcode project that wraps the prebuilt ExecuTorch
+ static archives into `ExecutorchLib.xcframework` (used only when (re)building
+ the iOS release artifacts).
+- `common/phonemis` β git submodule (TTS phonemizer), built from source. Run
+ `git submodule update --init` to fetch it.
-For now these can be obtained from the PoC's `third-party/` directory:
-https://github.com/barhanc/rnet-poc/tree/main/third-party
+## Downloaded on demand (NOT committed)
-These must be provisioned before an on-device native build. Wiring this into the
-repo's on-demand artifact mechanism is tracked as a follow-up (see #1208). CI does
-not require them β it builds and type-checks the TypeScript only.
+`scripts/download-libs.js` runs at **postinstall** and downloads the prebuilt
+binaries from this repo's GitHub Releases (tag `v`), based on the
+app's opted-in `backends` / `libs` / `features` (see the getting-started docs).
+It writes `rne-build-config.json`, which the podspec and `android/build.gradle.kts`
+read to gate `RNE_ENABLE_*` and link only the requested backends.
-The `common/phonemis` git submodule is retained for later use (TTS phonemizer);
-run `git submodule update --init` to fetch it.
+Extracted layout the podspec / `android/CMakeLists.txt` expect:
+
+- `include/` β ExecuTorch + c10 + torch + `pytorch/tokenizers` + opencv headers
+ (from `headers.tar.gz`, always downloaded; platform-independent)
+- `android/libs/executorch//libexecutorch.so` (+ `libxnnpack_executorch_backend.so`,
+ `libvulkan_executorch_backend.so` when those backends are enabled)
+- `android/libs/opencv//*.a`, `android/libs/opencv-third-party//libkleidicv*.a`
+ (when opencv is enabled)
+- `ios/ExecutorchLib.xcframework`, `ios/libs/executorch/*.a` (+ `mlx.metallib`),
+ `ios/libs/{pthreadpool,cpuinfo}`
+- `ios/{Xnnpack,CoreML,MLX}Backend.xcframework` (per enabled backend; MLX is the
+ device slice only)
+
+iOS OpenCV is provided via the `opencv-rne` CocoaPod, not a downloaded tarball.
+
+## Provisioning / overrides (env vars for `download-libs.js`)
+
+- `RNET_SKIP_DOWNLOAD=1` β skip download (CI / pre-cached libs); still writes config.
+- `RNET_HEADERS_ONLY=1` β fetch only `headers.tar.gz` (no native libs), e.g. for
+ clang-tidy / IDE tooling that needs include paths but never links the binaries.
+- `RNET_NO_X86_64=1` β skip the `android-x86_64` (emulator) ABI.
+- `RNET_BASE_URL=` β override the release base URL (e.g. a local
+ `python3 -m http.server` for testing).
+- `RNET_TARGET=` / `RNET_LIBS_CACHE_DIR=` / `GITHUB_TOKEN=`.
+
+The release tarballs are produced from the
+`software-mansion-labs/executorch@ms/separate-backends` build via
+`scripts/package-release-artifacts.sh`.
diff --git a/packages/react-native-executorch/third-party/ios/ExecutorchLib/ExecutorchLib.xcodeproj/project.pbxproj b/packages/react-native-executorch/third-party/ios/ExecutorchLib/ExecutorchLib.xcodeproj/project.pbxproj
new file mode 100644
index 0000000000..a7c0ce85bc
--- /dev/null
+++ b/packages/react-native-executorch/third-party/ios/ExecutorchLib/ExecutorchLib.xcodeproj/project.pbxproj
@@ -0,0 +1,491 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 56;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 0E4A7F472D67549100D8DCBA /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E4A7F442D67549100D8DCBA /* Metal.framework */; };
+ 0E4A7F482D67549100D8DCBA /* MetalPerformanceShaders.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E4A7F452D67549100D8DCBA /* MetalPerformanceShaders.framework */; };
+ 0E4A7F492D67549100D8DCBA /* MetalPerformanceShadersGraph.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0E4A7F462D67549100D8DCBA /* MetalPerformanceShadersGraph.framework */; };
+ 5576B4B92CEF970E005027B7 /* ETModel.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5576B4B82CEF970C005027B7 /* ETModel.mm */; };
+ 55EA2C572CB90E7D004315B3 /* Accelerate.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55EA2C562CB90E7D004315B3 /* Accelerate.framework */; };
+ 55EA2C592CB90E80004315B3 /* CoreML.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 55EA2C582CB90E80004315B3 /* CoreML.framework */; };
+ 55EA2C5B2CB90E85004315B3 /* libsqlite3.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 55EA2C5A2CB90E85004315B3 /* libsqlite3.tbd */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXFileReference section */
+ 0E4A7F442D67549100D8DCBA /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
+ 0E4A7F452D67549100D8DCBA /* MetalPerformanceShaders.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalPerformanceShaders.framework; path = System/Library/Frameworks/MetalPerformanceShaders.framework; sourceTree = SDKROOT; };
+ 0E4A7F462D67549100D8DCBA /* MetalPerformanceShadersGraph.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MetalPerformanceShadersGraph.framework; path = System/Library/Frameworks/MetalPerformanceShadersGraph.framework; sourceTree = SDKROOT; };
+ 5576B4B62CEF9705005027B7 /* ETModel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ETModel.h; sourceTree = ""; };
+ 5576B4B82CEF970C005027B7 /* ETModel.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ETModel.mm; sourceTree = ""; };
+ 55EA2C1C2CB90C22004315B3 /* ExecutorchLib.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ExecutorchLib.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 55EA2C562CB90E7D004315B3 /* Accelerate.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Accelerate.framework; path = System/Library/Frameworks/Accelerate.framework; sourceTree = SDKROOT; };
+ 55EA2C582CB90E80004315B3 /* CoreML.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreML.framework; path = System/Library/Frameworks/CoreML.framework; sourceTree = SDKROOT; };
+ 55EA2C5A2CB90E85004315B3 /* libsqlite3.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libsqlite3.tbd; path = usr/lib/libsqlite3.tbd; sourceTree = SDKROOT; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 55EA2C192CB90C22004315B3 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 0E4A7F472D67549100D8DCBA /* Metal.framework in Frameworks */,
+ 0E4A7F482D67549100D8DCBA /* MetalPerformanceShaders.framework in Frameworks */,
+ 0E4A7F492D67549100D8DCBA /* MetalPerformanceShadersGraph.framework in Frameworks */,
+ 55EA2C5B2CB90E85004315B3 /* libsqlite3.tbd in Frameworks */,
+ 55EA2C592CB90E80004315B3 /* CoreML.framework in Frameworks */,
+ 55EA2C572CB90E7D004315B3 /* Accelerate.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 55EA2C122CB90C22004315B3 = {
+ isa = PBXGroup;
+ children = (
+ 55EA2C1E2CB90C22004315B3 /* ExecutorchLib */,
+ 55EA2C1D2CB90C22004315B3 /* Products */,
+ 55EA2C552CB90E7D004315B3 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 55EA2C1D2CB90C22004315B3 /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 55EA2C1C2CB90C22004315B3 /* ExecutorchLib.framework */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 55EA2C1E2CB90C22004315B3 /* ExecutorchLib */ = {
+ isa = PBXGroup;
+ children = (
+ 55EA2C352CB90C7A004315B3 /* Exported */,
+ );
+ path = ExecutorchLib;
+ sourceTree = "";
+ };
+ 55EA2C352CB90C7A004315B3 /* Exported */ = {
+ isa = PBXGroup;
+ children = (
+ 5576B4B82CEF970C005027B7 /* ETModel.mm */,
+ 5576B4B62CEF9705005027B7 /* ETModel.h */,
+ );
+ path = Exported;
+ sourceTree = "";
+ };
+ 55EA2C552CB90E7D004315B3 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 0E4A7F442D67549100D8DCBA /* Metal.framework */,
+ 0E4A7F452D67549100D8DCBA /* MetalPerformanceShaders.framework */,
+ 0E4A7F462D67549100D8DCBA /* MetalPerformanceShadersGraph.framework */,
+ 55EA2C5A2CB90E85004315B3 /* libsqlite3.tbd */,
+ 55EA2C582CB90E80004315B3 /* CoreML.framework */,
+ 55EA2C562CB90E7D004315B3 /* Accelerate.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 55EA2C1B2CB90C22004315B3 /* ExecutorchLib */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 55EA2C232CB90C22004315B3 /* Build configuration list for PBXNativeTarget "ExecutorchLib" */;
+ buildPhases = (
+ 55EA2C182CB90C22004315B3 /* Sources */,
+ 55EA2C192CB90C22004315B3 /* Frameworks */,
+ 55EA2C1A2CB90C22004315B3 /* Resources */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = ExecutorchLib;
+ packageProductDependencies = (
+ );
+ productName = ExecutorchLib;
+ productReference = 55EA2C1C2CB90C22004315B3 /* ExecutorchLib.framework */;
+ productType = "com.apple.product-type.framework";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 55EA2C132CB90C22004315B3 /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = 1;
+ LastUpgradeCheck = 1540;
+ TargetAttributes = {
+ 55EA2C1B2CB90C22004315B3 = {
+ CreatedOnToolsVersion = 15.4;
+ };
+ };
+ };
+ buildConfigurationList = 55EA2C162CB90C22004315B3 /* Build configuration list for PBXProject "ExecutorchLib" */;
+ compatibilityVersion = "Xcode 14.0";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 55EA2C122CB90C22004315B3;
+ packageReferences = (
+ );
+ productRefGroup = 55EA2C1D2CB90C22004315B3 /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 55EA2C1B2CB90C22004315B3 /* ExecutorchLib */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 55EA2C1A2CB90C22004315B3 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 55EA2C182CB90C22004315B3 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 5576B4B92CEF970E005027B7 /* ETModel.mm in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin XCBuildConfiguration section */
+ 55EA2C212CB90C22004315B3 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
+ MTL_FAST_MATH = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Debug;
+ };
+ 55EA2C222CB90C22004315B3 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_ENABLE_OBJC_WEAK = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ COPY_PHASE_STRIP = NO;
+ CURRENT_PROJECT_VERSION = 1;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu17;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ MTL_FAST_MATH = YES;
+ SDKROOT = iphoneos;
+ VALIDATE_PRODUCT = YES;
+ VERSIONING_SYSTEM = "apple-generic";
+ VERSION_INFO_PREFIX = "";
+ };
+ name = Release;
+ };
+ 55EA2C242CB90C22004315B3 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO;
+ BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = B357MU264T;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ EXCLUDED_ARCHS = x86_64;
+ GENERATE_INFOPLIST_FILE = YES;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(TEMP_DIR)/cmake/include",
+ );
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(TEMP_DIR)/cmake/lib",
+ );
+ MARKETING_VERSION = 1.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ "OTHER_LDFLAGS[sdk=iphoneos*]" = (
+ "$(inherited)",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_optimized_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_llm_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_torchao_ios.a",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkleidiai_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_quantized_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libbackend_mps_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_llm_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libthreadpool_ios.a",
+ );
+ "OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
+ "$(inherited)",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_optimized_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_llm_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_torchao_simulator.a",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkleidiai_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_quantized_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libbackend_mps_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_llm_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libthreadpool_simulator.a",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.Executorch;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 55EA2C252CB90C22004315B3 /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALLOW_TARGET_PLATFORM_SPECIALIZATION = NO;
+ BUILD_LIBRARY_FOR_DISTRIBUTION = NO;
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ DEFINES_MODULE = YES;
+ DEVELOPMENT_TEAM = B357MU264T;
+ DYLIB_COMPATIBILITY_VERSION = 1;
+ DYLIB_CURRENT_VERSION = 1;
+ DYLIB_INSTALL_NAME_BASE = "@rpath";
+ ENABLE_MODULE_VERIFIER = YES;
+ EXCLUDED_ARCHS = x86_64;
+ GENERATE_INFOPLIST_FILE = YES;
+ HEADER_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(TEMP_DIR)/cmake/include",
+ );
+ INFOPLIST_KEY_NSHumanReadableCopyright = "";
+ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks";
+ IPHONEOS_DEPLOYMENT_TARGET = 17.0;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ "@loader_path/Frameworks",
+ );
+ LIBRARY_SEARCH_PATHS = (
+ "$(inherited)",
+ "$(TEMP_DIR)/cmake/lib",
+ );
+ MARKETING_VERSION = 1.0;
+ MODULE_VERIFIER_SUPPORTED_LANGUAGES = "objective-c objective-c++";
+ MODULE_VERIFIER_SUPPORTED_LANGUAGE_STANDARDS = "gnu17 gnu++20";
+ "OTHER_LDFLAGS[sdk=iphoneos*]" = (
+ "$(inherited)",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_optimized_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_llm_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_torchao_ios.a",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkleidiai_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_quantized_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libbackend_mps_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_llm_ios.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libthreadpool_ios.a",
+ );
+ "OTHER_LDFLAGS[sdk=iphonesimulator*]" = (
+ "$(inherited)",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_optimized_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_llm_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_torchao_simulator.a",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkleidiai_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libkernels_quantized_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libbackend_mps_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libexecutorch_llm_simulator.a",
+ "-force_load",
+ "$(PROJECT_DIR)/../../../third-party/ios/libs/executorch/libthreadpool_simulator.a",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.swmansion.Executorch;
+ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
+ SKIP_INSTALL = YES;
+ SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
+ SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SWIFT_EMIT_LOC_STRINGS = YES;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 55EA2C162CB90C22004315B3 /* Build configuration list for PBXProject "ExecutorchLib" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 55EA2C212CB90C22004315B3 /* Debug */,
+ 55EA2C222CB90C22004315B3 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 55EA2C232CB90C22004315B3 /* Build configuration list for PBXNativeTarget "ExecutorchLib" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 55EA2C242CB90C22004315B3 /* Debug */,
+ 55EA2C252CB90C22004315B3 /* Release */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 55EA2C132CB90C22004315B3 /* Project object */;
+}
diff --git a/packages/react-native-executorch/third-party/ios/ExecutorchLib/build.sh b/packages/react-native-executorch/third-party/ios/ExecutorchLib/build.sh
new file mode 100755
index 0000000000..73a9c908f3
--- /dev/null
+++ b/packages/react-native-executorch/third-party/ios/ExecutorchLib/build.sh
@@ -0,0 +1,108 @@
+#!/bin/bash
+
+# Builds ExecutorchLib.xcframework for iOS and iOS Simulator, plus separate
+# static xcframeworks for optional backends (xnnpack, coreml, mlx).
+#
+# This script:
+# 1. Cleans previous builds
+# 2. Archives the framework for iOS device (arm64)
+# 3. Archives the framework for iOS Simulator (arm64)
+# 4. Combines both archives into a single .xcframework
+# 5. Creates XnnpackBackend.xcframework from the backend .a files
+# 6. Creates CoreMLBackend.xcframework from the backend .a files
+# 7. Creates MLXBackend.xcframework from the backend .a file (iOS device only β
+# the iOS simulator cannot drive MLX-on-Metal, so no simulator slice is built)
+#
+# Output:
+# ./output/ExecutorchLib.xcframework
+# ./output/XnnpackBackend.xcframework
+# ./output/CoreMLBackend.xcframework
+# ./output/MLXBackend.xcframework
+#
+# Usage: ./build.sh
+
+set -euo pipefail
+
+# --- Configuration ---
+PROJECT_NAME="ExecutorchLib"
+SCHEME_NAME="ExecutorchLib"
+OUTPUT_FOLDER="output"
+LIBS_DIR="$(pwd)/../../../third-party/ios/libs/executorch"
+
+# --- Derived Variables ---
+BUILD_FOLDER="build"
+ARCHIVE_PATH_IOS="$BUILD_FOLDER/$SCHEME_NAME-iOS"
+ARCHIVE_PATH_SIMULATOR="$BUILD_FOLDER/$SCHEME_NAME-iOS_Simulator"
+FRAMEWORK_NAME="$SCHEME_NAME.framework"
+XCFRAMEWORK_NAME="$SCHEME_NAME.xcframework"
+XCFRAMEWORK_PATH="$OUTPUT_FOLDER/$XCFRAMEWORK_NAME"
+
+# --- Script ---
+rm -rf "$BUILD_FOLDER" "$OUTPUT_FOLDER"
+mkdir -p "$OUTPUT_FOLDER"
+
+xcodebuild clean -project "$PROJECT_NAME.xcodeproj" -scheme "$SCHEME_NAME"
+
+xcodebuild archive \
+ -project "$PROJECT_NAME.xcodeproj" \
+ -scheme "$SCHEME_NAME" \
+ -configuration Release \
+ -destination "generic/platform=iOS" \
+ -archivePath "$ARCHIVE_PATH_IOS" \
+ SKIP_INSTALL=NO \
+ BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
+ CODE_SIGNING_ALLOWED=NO
+
+xcodebuild archive \
+ -project "$PROJECT_NAME.xcodeproj" \
+ -scheme "$SCHEME_NAME" \
+ -configuration Release \
+ -destination "generic/platform=iOS Simulator" \
+ -archivePath "$ARCHIVE_PATH_SIMULATOR" \
+ SKIP_INSTALL=NO \
+ BUILD_LIBRARY_FOR_DISTRIBUTION=YES \
+ CODE_SIGNING_ALLOWED=NO
+
+xcodebuild -create-xcframework \
+ -framework "$ARCHIVE_PATH_IOS.xcarchive/Products/Library/Frameworks/$FRAMEWORK_NAME" \
+ -framework "$ARCHIVE_PATH_SIMULATOR.xcarchive/Products/Library/Frameworks/$FRAMEWORK_NAME" \
+ -output "$XCFRAMEWORK_PATH"
+
+# --- Build XnnpackBackend.xcframework ---
+# CocoaPods requires matching binary names across slices
+STAGING=$(mktemp -d)
+mkdir -p "$STAGING/ios" "$STAGING/sim"
+cp "$LIBS_DIR/libbackend_xnnpack_ios.a" "$STAGING/ios/libXnnpackBackend.a"
+cp "$LIBS_DIR/libbackend_xnnpack_simulator.a" "$STAGING/sim/libXnnpackBackend.a"
+xcodebuild -create-xcframework \
+ -library "$STAGING/ios/libXnnpackBackend.a" \
+ -library "$STAGING/sim/libXnnpackBackend.a" \
+ -output "$OUTPUT_FOLDER/XnnpackBackend.xcframework"
+
+# --- Build CoreMLBackend.xcframework ---
+cp "$LIBS_DIR/libbackend_coreml_ios.a" "$STAGING/ios/libCoreMLBackend.a"
+cp "$LIBS_DIR/libbackend_coreml_simulator.a" "$STAGING/sim/libCoreMLBackend.a"
+xcodebuild -create-xcframework \
+ -library "$STAGING/ios/libCoreMLBackend.a" \
+ -library "$STAGING/sim/libCoreMLBackend.a" \
+ -output "$OUTPUT_FOLDER/CoreMLBackend.xcframework"
+
+# --- Build MLXBackend.xcframework (iOS device only) ---
+# MLX uses Metal APIs absent from iPhoneSimulator.sdk and the iOS simulator
+# cannot drive MLX-on-Metal, so we build and ship the device slice only.
+if [ -f "$LIBS_DIR/libbackend_mlx_ios.a" ]; then
+ cp "$LIBS_DIR/libbackend_mlx_ios.a" "$STAGING/ios/libMLXBackend.a"
+ xcodebuild -create-xcframework \
+ -library "$STAGING/ios/libMLXBackend.a" \
+ -output "$OUTPUT_FOLDER/MLXBackend.xcframework"
+ echo "Built MLXBackend.xcframework (device slice only)"
+else
+ echo "Skipped MLXBackend.xcframework (libbackend_mlx_ios.a not present)"
+fi
+rm -rf "$STAGING"
+
+echo "Done! Output:"
+echo " $OUTPUT_FOLDER/ExecutorchLib.xcframework"
+echo " $OUTPUT_FOLDER/XnnpackBackend.xcframework"
+echo " $OUTPUT_FOLDER/CoreMLBackend.xcframework"
+[ -d "$OUTPUT_FOLDER/MLXBackend.xcframework" ] && echo " $OUTPUT_FOLDER/MLXBackend.xcframework" || true
diff --git a/packages/react-native-executorch/tsconfig.doc.json b/packages/react-native-executorch/tsconfig.doc.json
new file mode 100644
index 0000000000..6104102d09
--- /dev/null
+++ b/packages/react-native-executorch/tsconfig.doc.json
@@ -0,0 +1,9 @@
+{
+ "extends": "./tsconfig.json",
+ "include": ["src/**/*"],
+ "compilerOptions": {
+ "noEmit": true,
+ "skipLibCheck": true,
+ "jsx": "react"
+ }
+}