Skip to content

Commit e69584e

Browse files
committed
feat: on-demand native lib download with opt-in feature splitting
The npm tarball ships without prebuilt native binaries — they are downloaded from GitHub Releases at postinstall and extracted into third-party/, where the existing CMake / podspec configurations pick them up unchanged. Apps can opt out of features they don't need to skip both the download and the native compilation: "react-native-executorch": { "extras": ["opencv", "phonemizer", "xnnpack", "coreml", "vulkan"] } Defaults to all enabled. Each extra trims one or more artifacts and toggles a corresponding RNE_ENABLE_* CMake / podspec flag, dropping its sources from compilation and its libraries from the final link. Per-platform behavior: - opencv Android + iOS (iOS provided via opencv-rne CocoaPod) - phonemizer Android + iOS - xnnpack iOS-only as a force-loaded XnnpackBackend.xcframework; baked into libexecutorch.so on Android - coreml iOS-only as a force-loaded CoreMLBackend.xcframework - vulkan Android-only as a separately-loaded libvulkan_executorch_backend.so Vulkan ships as its own shared library (mirroring the QNN backend pattern) so its load-time backend registration runs only when the user opts in. The .so links only against vulkan_backend + vulkan_schema + executorch_core, not the CPU kernel registries, so it does not cause duplicate kernel registration when loaded alongside libexecutorch.so.
1 parent e937c36 commit e69584e

54 files changed

Lines changed: 1201 additions & 145 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,15 @@ docs/docs/06-api-reference/
9898
# integration test model assets
9999
packages/react-native-executorch/common/rnexecutorch/tests/integration/assets/models/
100100

101+
# release artifact staging dir (produced by scripts/package-release-artifacts.sh)
102+
packages/react-native-executorch/dist-artifacts/
103+
104+
# on-demand native libs (downloaded at postinstall time, not committed)
105+
packages/react-native-executorch/third-party/android/libs/
106+
packages/react-native-executorch/third-party/ios/ExecutorchLib.xcframework/
107+
packages/react-native-executorch/third-party/ios/libs/
108+
packages/react-native-executorch/rne-build-config.json
109+
101110
# custom
102111
*.tgz
103112
Makefile

docs/docs/01-fundamentals/01-getting-started.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,42 @@ Installation is pretty straightforward, use your package manager of choice to in
7676
</TabItem>
7777
</Tabs>
7878

79+
### Configuring backends and extras
80+
81+
On install, `react-native-executorch` runs a `postinstall` script that downloads prebuilt native libraries from the matching GitHub Release and unpacks them under `third-party/`. By default every optional feature is included — which keeps the app binary large. You can opt out of anything you don't need by adding an `extras` array to your app's `package.json`:
82+
83+
```json
84+
{
85+
"react-native-executorch": {
86+
"extras": ["xnnpack", "coreml", "vulkan", "opencv", "phonemizer"]
87+
}
88+
}
89+
```
90+
91+
If the `extras` key is omitted, all five features are enabled. To disable a feature, drop its name from the array.
92+
93+
| Extra | iOS | Android | What it enables |
94+
| ------------ | --------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------- |
95+
| `opencv` | ✅ (via the `opencv-rne` CocoaPod) || Computer-vision models (classification, detection, OCR, etc.) |
96+
| `phonemizer` ||| Text-to-speech models |
97+
| `xnnpack` | ✅ — `XnnpackBackend.xcframework` force-loaded into the app | always on — XNNPACK is baked into `libexecutorch.so`; the flag has no effect on Android | XNNPACK CPU backend (required for most quantized models) |
98+
| `coreml` | ✅ — `CoreMLBackend.xcframework` force-loaded into the app | n/a (CoreML is iOS-only) | Core ML backend (Apple Neural Engine / GPU acceleration) |
99+
| `vulkan` | n/a (Vulkan is Android-only) | ✅ — separately-loaded `libvulkan_executorch_backend.so` | Vulkan GPU backend |
100+
101+
Source files and native libraries are excluded from compilation when an extra is disabled, so builds that only need LLMs can skip OpenCV and cut tens of megabytes off the final binary.
102+
103+
The postinstall step honors a few environment variables:
104+
105+
| Variable | Purpose |
106+
| ---------------------- | ------------------------------------------------------------------------- |
107+
| `RNET_SKIP_DOWNLOAD=1` | Skip the download entirely (for CI with pre-cached libraries). |
108+
| `RNET_LIBS_CACHE_DIR` | Custom cache directory (default: `~/.cache/react-native-executorch/<v>`). |
109+
| `RNET_TARGET` | Force a specific target, e.g. `android-arm64-v8a` or `ios`. |
110+
| `RNET_NO_X86_64=1` | Skip the Android x86_64 tarball (handy when only building for a device). |
111+
| `GITHUB_TOKEN` | Required to access draft releases while iterating on a new version. |
112+
113+
After changing `extras`, re-run `yarn install` (or the equivalent) so the postinstall script regenerates `rne-build-config.json` and re-extracts the right tarballs, then rebuild the native project.
114+
79115
:::warning
80116
Before using any other API, you must call `initExecutorch` with a resource fetcher adapter at the entry point of your app:
81117

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# Native libraries pipeline
2+
3+
This document describes how native dependencies (ExecuTorch runtime, backends, OpenCV, phonemizer) are produced, shipped, and stitched into an app build. It is intended for maintainers — the user-facing summary lives in `docs/docs/01-fundamentals/01-getting-started.md`.
4+
5+
## High-level flow
6+
7+
```
8+
┌──────────────────────┐ ┌────────────────────────┐ ┌───────────────────────┐
9+
│ ExecuTorch fork │ ───▶ │ GitHub Release v<ver> │ ───▶ │ postinstall script │
10+
│ + our patches │ │ <artifact>.tar.gz │ │ download-libs.js │
11+
│ (separate repo) │ │ <artifact>.tar.gz.256 │ │ │
12+
└──────────────────────┘ └────────────────────────┘ └───────────┬───────────┘
13+
14+
15+
┌───────────────────────┐
16+
│ third-party/android │
17+
│ third-party/ios │
18+
│ rne-build-config.json│
19+
└───────────┬───────────┘
20+
21+
┌───────────────────────────┴────────────────────────────┐
22+
▼ ▼
23+
┌───────────────────────┐ ┌─────────────────────────┐
24+
│ android/build.gradle │ │ react-native-executorch │
25+
│ + CMakeLists.txt │ │ .podspec │
26+
│ -DRNE_ENABLE_* │ │ -DRNE_ENABLE_* │
27+
└───────────────────────┘ │ force_load xcframeworks │
28+
└─────────────────────────┘
29+
```
30+
31+
## Install-time: `scripts/download-libs.js`
32+
33+
Runs at `postinstall`. Responsibilities:
34+
35+
1. Read `react-native-executorch.extras` from the app's `package.json` (uses `INIT_CWD`). Defaults to `["opencv", "phonemizer", "xnnpack", "coreml", "vulkan"]`.
36+
2. Write `rne-build-config.json` at the package root with boolean flags — this file is the single source of truth consumed by both the Gradle build and the podspec.
37+
3. Detect targets (`ios` on macOS; always `android-arm64-v8a` and, unless `RNET_NO_X86_64` is set, `android-x86_64`).
38+
4. For each target × enabled extra, fetch the corresponding `<artifact>.tar.gz` from the GitHub Release tagged `v${PACKAGE_VERSION}`, verify the `.sha256`, and extract into `third-party/android/libs/` or `third-party/ios/`.
39+
5. Cache validated tarballs under `~/.cache/react-native-executorch/<version>/` so subsequent installs skip the network.
40+
41+
Environment overrides: `RNET_SKIP_DOWNLOAD`, `RNET_LIBS_CACHE_DIR`, `RNET_TARGET`, `RNET_BASE_URL` (useful with `python3 -m http.server` against `dist-artifacts/` for local iteration), `GITHUB_TOKEN` (needed for draft releases).
42+
43+
The set of artifacts per target is defined in `getArtifacts()`:
44+
45+
| Artifact name | Target | Produced by | Contents |
46+
| ---------------------------------------- | ------- | ---------------------------------------- | ------------------------------------------------------------- |
47+
| `core-android-arm64-v8a` | Android | ExecuTorch fork build | `libexecutorch.so` (XNNPACK baked in, no Vulkan), headers |
48+
| `core-android-x86_64` | Android | ExecuTorch fork build | x86_64 `libexecutorch.so` for the simulator |
49+
| `vulkan-android-*` | Android | ExecuTorch fork build | `libvulkan_executorch_backend.so` (separately-loaded) |
50+
| `core-ios` | iOS | `third-party/ios/ExecutorchLib/build.sh` | `ExecutorchLib.xcframework` |
51+
| `xnnpack-ios` | iOS | `third-party/ios/ExecutorchLib/build.sh` | `XnnpackBackend.xcframework` |
52+
| `coreml-ios` | iOS | `third-party/ios/ExecutorchLib/build.sh` | `CoreMLBackend.xcframework` |
53+
| `opencv-android-*` | Android | OpenCV release process | Static OpenCV + KleidiCV HAL |
54+
| `phonemizer-android-*`, `phonemizer-ios` | both | phonemizer build | `libphonemis.a` (iOS: physical + simulator) |
55+
56+
There is no `xnnpack-android-*` tarball: XNNPACK is whole-archive-linked into `libexecutorch.so` at ExecuTorch fork build time, so the `xnnpack` extra has no effect on Android (the postinstall script logs a warning when the user disables it on Android).
57+
58+
(`opencv-ios` is not a tarball — iOS consumes OpenCV through the `opencv-rne` CocoaPod.)
59+
60+
## Build-time: Android
61+
62+
`android/build.gradle` reads `rne-build-config.json` once and forwards the booleans to CMake:
63+
64+
```groovy
65+
"-DRNE_ENABLE_OPENCV=${rneBuildConfig.enableOpencv ? 'ON' : 'OFF'}",
66+
"-DRNE_ENABLE_PHONEMIZER=${rneBuildConfig.enablePhonemizer ? 'ON' : 'OFF'}",
67+
"-DRNE_ENABLE_VULKAN=${rneBuildConfig.enableVulkan ? 'ON' : 'OFF'}"
68+
```
69+
70+
`android/CMakeLists.txt` and `android/src/main/cpp/CMakeLists.txt` respond by:
71+
72+
- Adding `-DRNE_ENABLE_OPENCV` / `-DRNE_ENABLE_PHONEMIZER` compile definitions so C++ code can `#ifdef` around optional dependencies.
73+
- Conditionally linking `libopencv_*.a`, KleidiCV HAL (arm64 only), and `libphonemis.a`.
74+
- Always linking against the prebuilt `libexecutorch.so` downloaded into `third-party/android/libs/executorch/<abi>/`.
75+
- When `RNE_ENABLE_VULKAN=ON`, importing `libvulkan_executorch_backend.so` and linking `react-native-executorch.so` against it. Linking (rather than dynamic `dlopen`) lets Gradle bundle the `.so` into the APK and triggers the dynamic linker to load it whenever `libreact-native-executorch.so` is loaded — its load-time constructor then registers the Vulkan backend with the runtime in `libexecutorch.so`.
76+
77+
**XNNPACK is baked into `libexecutorch.so` on Android** (not a separate shared library). The `xnnpack` extra has no effect on Android; the postinstall script logs a warning when the user disables it for an Android target. **Vulkan is the one backend that ships separately** — see the "Why backends differ" section for the design choice.
78+
79+
## Build-time: iOS
80+
81+
`react-native-executorch.podspec` reads the same `rne-build-config.json` and:
82+
83+
- Excludes opencv/phonemizer C++ sources from compilation when those extras are off.
84+
- Appends `-DRNE_ENABLE_*` to `OTHER_CPLUSPLUSFLAGS`.
85+
- Assembles `OTHER_LDFLAGS[sdk=iphoneos*]` and `OTHER_LDFLAGS[sdk=iphonesimulator*]` with `-force_load` entries for each enabled backend xcframework.
86+
- Declares `ExecutorchLib.xcframework` in `vendored_frameworks` but _not_ the backend xcframeworks — backend xcframeworks only live on the linker command line, never in the CocoaPods vendoring list (see next section for why).
87+
- Adds `sqlite3` and the `CoreML` system framework to linkage only when Core ML is enabled.
88+
89+
## Why backends differ between platforms
90+
91+
ExecuTorch registers kernels statically via `__attribute__((constructor))` functions inside each backend's `.a`/`.so`. Two design points fall out of this:
92+
93+
1. **Force-load is required.** Linkers drop unreferenced object files. The registrar symbols have no external users (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, or `executorch_target_link_options_shared_lib(...)` in ExecuTorch's own CMake helpers).
94+
95+
2. **A single copy of each CPU-kernel registration must exist.** Multiple backend libraries that each whole-archive-link `optimized_native_cpu_ops_lib` cause duplicate kernel-registration aborts (`error 22 EEXIST`) when both get force-loaded into the same process.
96+
97+
On **iOS**, each backend ships as its own static xcframework (`XnnpackBackend.xcframework`, `CoreMLBackend.xcframework`). The podspec force-loads only the ones the user opted into, and `ExecutorchLib.xcframework` itself does not whole-archive the CPU ops — so there is no duplicate registration.
98+
99+
On **Android**, the first iteration tried the same split for both XNNPACK and Vulkan and hit the duplicate-registration abort because the ExecuTorch Android build whole-archive-linked the CPU ops (`optimized_native_cpu_ops_lib`, `custom_ops`, `quantized_ops_lib`, `register_prim_ops`) into each backend shared library. The fix is in the ExecuTorch fork: the new `EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED` switch builds `libvulkan_executorch_backend.so` linking only `vulkan_backend (--whole-archive)` + `vulkan_schema` + `executorch_core` — no kernel-registration archives, so loading it on top of `libexecutorch.so` does not duplicate any registration. XNNPACK stays baked into `libexecutorch.so` (the same separation could be done for it, but is not yet wired up — separating XNNPACK on Android is tracked as a follow-up).
100+
101+
This is why Android publishes `core-android-*` (XNNPACK baked) plus an opt-in `vulkan-android-*` tarball, while iOS has separate `core-ios`, `xnnpack-ios`, and `coreml-ios` tarballs.
102+
103+
## Building artifacts from the ExecuTorch fork
104+
105+
Patched sources live in a separate repo (see `executorch/` in the maintainer's machine, typically checked out next to `react-native-executorch/`). The fork branch is [`msluszniak/executorch@ms/separate-backends`](https://github.com/msluszniak/executorch/tree/@ms/separate-backends) and carries three commits:
106+
107+
- **chore: remove version script from `executorch_jni`** — reverts the Feb 2026 symbol-hiding change so RNE's C++ layer can resolve `Module`, `threadpool`, etc. directly.
108+
- **feat: build `vulkan_backend` as a separate shared library on Android** — adds the `EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED` CMake option. When ON, `libvulkan_executorch_backend.so` is produced alongside `libexecutorch_jni.so` instead of vulkan_backend being whole-archive-linked into the latter. Mirrors the QNN backend pattern.
109+
- **build: disable `-Werror` for `flatcc_ep` on host clang** — Apple clang 21+ flags warnings flatcc has not yet cleaned up; needed to build on Xcode 26.4+.
110+
111+
### iOS
112+
113+
From inside `packages/react-native-executorch/third-party/ios/ExecutorchLib/`:
114+
115+
```bash
116+
./build.sh
117+
```
118+
119+
The script drives Xcode to archive the Obj-C++ wrapper for device and simulator, then uses `xcodebuild -create-xcframework` to produce:
120+
121+
- `output/ExecutorchLib.xcframework` — the high-level wrapper + ExecuTorch core + baked-in CPU ops.
122+
- `output/XnnpackBackend.xcframework` — repackaged from `third-party/ios/libs/executorch/libbackend_xnnpack_{ios,simulator}.a`.
123+
- `output/CoreMLBackend.xcframework` — repackaged from `libbackend_coreml_{ios,simulator}.a`.
124+
125+
Producing the underlying `.a` files (executorch + backend static libs for both slices) is a separate step inside the ExecuTorch fork, outside the scope of this script — run the fork's iOS build instructions with XNNPACK and Core ML enabled, then drop the resulting `.a` files into `third-party/ios/libs/executorch/` before invoking `build.sh`.
126+
127+
CocoaPods constraint: inside an xcframework, the library file name must be identical across slices, which is why `build.sh` copies each slice into a temp directory and renames before calling `-create-xcframework`. Do not skip this step.
128+
129+
### Android
130+
131+
Use `scripts/build_android_library.sh` from the fork (with the `@ms/separate-backends` branch checked out). It already passes the right preset and flags. Just enable the two extras we need:
132+
133+
```bash
134+
# from the executorch fork
135+
export ANDROID_NDK=$HOME/Library/Android/sdk/ndk/27.1.12297006
136+
EXECUTORCH_BUILD_VULKAN=ON \
137+
EXECUTORCH_BUILD_VULKAN_BACKEND_SHARED=ON \
138+
ANDROID_ABI=arm64-v8a ./scripts/build_android_library.sh # repeat with x86_64
139+
```
140+
141+
Outputs land in `cmake-out-android-<abi>/extension/android/`:
142+
143+
- `libexecutorch_jni.so` → copy to `third-party/android/libs/executorch/<abi>/libexecutorch.so` (note the rename).
144+
- `libvulkan_executorch_backend.so` → copy to the same directory under its own name.
145+
146+
Strip both with `$ANDROID_NDK/toolchains/llvm/prebuilt/*/bin/llvm-strip` before committing. The headers under `third-party/include/` must match the fork commit that produced the binary — a mismatch shows up as runtime `dlopen` / symbol errors.
147+
148+
### Packaging for a release
149+
150+
For each `<artifact>` tarball:
151+
152+
```bash
153+
tar -czf <artifact>.tar.gz -C <staging-dir> .
154+
sha256sum <artifact>.tar.gz > <artifact>.tar.gz.sha256 # or shasum -a 256
155+
```
156+
157+
Staging-dir layout must mirror the destination (`download-libs.js` extracts with `tar -xzf` into `third-party/android/libs/` or `third-party/ios/` without any path stripping). So `core-android-arm64-v8a.tar.gz` contains a top-level `executorch/arm64-v8a/libexecutorch.so`, `cpuinfo/arm64-v8a/libcpuinfo.a`, etc.
158+
159+
Upload every `<artifact>.tar.gz` **and** its `<artifact>.tar.gz.sha256` as release assets under the `v<version>` tag on GitHub. Publishing the release (out of draft) makes them fetchable anonymously; until then, consumers need `GITHUB_TOKEN` with `repo:read`.
160+
161+
### Iterating locally
162+
163+
Drop built artifacts (plus `.sha256` files) into `packages/react-native-executorch/dist-artifacts/`, then run a static server and point the script at it:
164+
165+
```bash
166+
cd packages/react-native-executorch/dist-artifacts
167+
python3 -m http.server 8080 &
168+
RNET_BASE_URL=http://localhost:8080 yarn install
169+
```
170+
171+
This skips GitHub entirely and re-extracts from the local tarballs — the same checksum verification still runs, so stale caches still get rejected.

0 commit comments

Comments
 (0)