Skip to content

[RNE Rewrite] feat: on-demand native lib download and backend splitting#1283

Merged
msluszniak merged 15 commits into
rne-rewritefrom
@ms/on-demand-native-libs
Jun 29, 2026
Merged

[RNE Rewrite] feat: on-demand native lib download and backend splitting#1283
msluszniak merged 15 commits into
rne-rewritefrom
@ms/on-demand-native-libs

Conversation

@msluszniak

@msluszniak msluszniak commented Jun 25, 2026

Copy link
Copy Markdown
Member

Description

Ports PR #1039's on-demand native-lib mechanism into the rewrite flow. Apps declare what they need via a react-native-executorch block (backends / libs / features) in package.json; the postinstall scripts/download-libs.js expands features via the forward-looking FEATURE_MAP, writes rne-build-config.json, and downloads only the requested per-backend artifacts from GitHub Releases into third-party/. The podspec and android/build.gradle.kts read that config to gate RNE_ENABLE_* and link backends as separate libs (Xnnpack/CoreML/MLX xcframeworks, lib*_executorch_backend.so); MLX is the iOS device slice only.

Adapted to the rewrite: cpp/ layout (opencv kept as an extensible source list, guarded in RnExecutorch.cpp), Kotlin-DSL gradle config read, single android/CMakeLists.txt. The scripts, FEATURE_MAP, and artifact contract are kept identical to #1039 so future rewrite work stays code-only. CI (TS-only) skips the download via RNET_SKIP_DOWNLOAD in the setup action.

Backed by software-mansion-labs/executorchrne-split-build (already ET 1.3.1). Release artifacts (device-only MLX) are built/uploaded separately; committed headers / executorch.jar / ExecutorchLib wrapper are provisioned alongside.

Introduces a breaking change?

  • Yes
  • No

Type of change

  • Bug fix (change which fixes an issue)
  • New feature (change which adds functionality)
  • Documentation update (improves or adds clarity to existing documentation)
  • Other (chores, tests, code style improvements etc.)

Tested on

  • iOS
  • Android

Testing instructions

  1. Config flow: set an app's react-native-executorch.features (e.g. ["llm"]), run yarn install, and check rne-build-config.json reflects it (enableOpencv:false, enableCoreml:false, enableXnnpack:true). Setting legacy extras errors.
  2. Download flow: RNET_BASE_URL=https://github.com/software-mansion/react-native-executorch/releases/download/v0.0.0-rewrite-libs-test RNET_TARGET=android-arm64-v8a INIT_CWD=<app> node scripts/download-libs.js — tarballs extract (checksum-verified) into third-party/android/libs/... exactly where CMakeLists.txt/podspec expect.
  3. CI gates: yarn lint, yarn workspace react-native-executorch prepare, yarn typecheck (all green; install with RNET_SKIP_DOWNLOAD=1).

Test artifacts uploaded to the v0.0.0-rewrite-libs-test pre-release: Android freshly built from rne-split-build (ET 1.3.1, split backends, arm64-v8a + x86_64); iOS reused from the same rne-split-build 1.3.1 lineage (#1039) with MLX stripped to the device slice only. Full download/extract/checksum flow verified across iOS + both Android ABIs. On-device app build still to be run.

Related issues

#1208

Checklist

  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have updated the documentation accordingly
  • My changes generate no new warnings

Additional notes

Draft: all of third-party/ (headers + binaries) is downloaded on demand, not committed — headers ship as a platform-independent headers.tar.gz alongside the binary artifacts (built with device-only MLX), uploaded separately.

Header provenance: headers.tar.gz is assembled from the rne-split-build ExecuTorch CMake install include tree (cmake-out*/include/: executorch, pytorch/{c10,torch,headeronly}, the tokenizer third-party headers absl/re2/nlohmann, and cpuinfo/pthreadpool — all platform-independent, identical across ABIs/SDKs) plus the opencv2 headers from the OpenCV prebuilt (same source as the opencv-rne pod / Android static libopencv_*.a, not built from executorch).

Docs getting-started is deferred (no rewrite getting-started page exists yet).

Port PR #1039's on-demand native-lib mechanism into the rewrite. Apps opt in
via a `react-native-executorch` block (`backends`/`libs`/`features`) in their
package.json; the postinstall `scripts/download-libs.js` expands `features` via
the full forward-looking FEATURE_MAP, writes `rne-build-config.json`, and
downloads only the requested per-backend artifacts from GitHub Releases into
third-party/. The podspec and android/build.gradle.kts read that config to gate
`RNE_ENABLE_*` and link backends as separate libs (Xnnpack/CoreML/MLX
xcframeworks, lib*_executorch_backend.so) — MLX device-slice only.

Adapted to the rewrite: cpp/ layout (opencv gated as an extensible source list,
guarded in RnExecutorch.cpp), Kotlin-DSL gradle config read, single
android/CMakeLists.txt. scripts + FEATURE_MAP + artifact contract kept identical
to #1039 so future rewrite work is code-only. CI (TS-only) skips the download
via RNET_SKIP_DOWNLOAD in the setup action.

Verified: config expansion, download/extract/checksum (local server), and CI
gates (lint, bob build, typecheck). Release artifacts (executorch fork build) +
committed headers/jar/ExecutorchLib wrapper are provisioned separately.
@msluszniak msluszniak force-pushed the @ms/on-demand-native-libs branch from 94f8547 to ab793e1 Compare June 25, 2026 11:43
Rebased onto rne-rewrite incl. #1280 keypoint detection. Rename the
poseEstimation FEATURE_MAP entry to keypointDetection (tracks the
useKeypointDetector hook) and set backends to xnnpack+coreml+mlx (RF-DETR
keypoint ships CoreML + MLX variants) + opencv. Declare it in the
computer-vision demo app.
Drop the committed third-party/include header set (~200K LOC of vendored
ExecuTorch/c10/torch/opencv headers). Headers are now downloaded like the
binaries: a platform-independent headers.tar.gz fetched by download-libs.js
and produced by package-release-artifacts.sh. Keeps the rewrite's 'no
third-party in git' philosophy and avoids churn on ExecuTorch bumps.
@msluszniak msluszniak force-pushed the @ms/on-demand-native-libs branch from ab793e1 to 45ec2cb Compare June 25, 2026 11:53
@msluszniak msluszniak self-assigned this Jun 25, 2026
@msluszniak msluszniak added refactoring improvement PRs or issues focused on improvements in the current codebase labels Jun 25, 2026
core-android/core-ios drop the separately-shipped pthreadpool+cpuinfo
(statically linked into libexecutorch.so / libthreadpool_*.a for the rewrite),
and the ABI-independent executorch.jar rides in the core-android-arm64 artifact
(downloaded to third-party, not committed).
Two gaps surfaced building apps/computer-vision on a physical device:
- libexecutorch.so references OpenMP runtime symbols (optimized kernels), so
  link -fopenmp -static-openmp into libRnExecutorch.so (matches main #1039).
- abiFilters was hardcoded to both ABIs, ignoring the app's
  reactNativeArchitectures; read it (filtered to arm64-v8a/x86_64) so device
  builds only compile + need the ABI they target. Build now succeeds, installs,
  and launches on device (arm64-v8a).
@msluszniak msluszniak marked this pull request as ready for review June 25, 2026 14:15
@msluszniak msluszniak requested a review from barhanc June 26, 2026 09:28
Add a Fundamentals docs tree to the rewrite (docs/docs was removed in the
scaffold):
- 01-getting-started: ported canonical content + a "Selecting native
  libraries" section covering the react-native-executorch package.json
  block (features/backends/libs) and the feature -> backend/lib mapping.
- 02-native-libraries: how the split is produced, shipped, and linked
  (download flow, artifact set, header provenance, Android/iOS wiring,
  force-load rationale, build recipe), corrected for the rewrite
  (@ms/separate-backends, ET 1.3.1, device-only MLX, downloaded headers).
- sidebars: current version exposes Fundamentals + API Reference; the
  not-yet-ported sections stay commented until their dirs are restored.

Fix the docs build, broken on the rewrite independently of these pages:
- restore packages/.../tsconfig.doc.json so typedoc can load the project.
- convert two URL-target {@link} TSDoc tags to markdown links so the
  generated API reference compiles under MDX.

Also fix a variable-shadowing lint error in download-libs.js and add the
new technical terms to the cspell wordlist.
Comment thread packages/react-native-executorch/scripts/download-libs.js
Comment thread packages/react-native-executorch/cpp/RnExecutorch.cpp Outdated
Comment thread packages/react-native-executorch/third-party/.gitignore
Comment thread packages/react-native-executorch/react-native-executorch.podspec Outdated
Comment thread packages/react-native-executorch/scripts/download-libs.js Outdated
@barhanc

barhanc commented Jun 26, 2026

Copy link
Copy Markdown
Contributor
  • Also, did you check the example app size with and without the split and how much it decreases it?
  • Would you be able to check that MLX backend gets correctly bundled for the computer-vision app (I don't have an iOS physical device available)?

  • Do we have some custom script for vendoring the executorch headers, because the headers downloaded using the split script do not match the ones copied directly from executorch repo used in rnet-poc, e.g. all llm runner helpers are missing?

…native-libs

# Conflicts:
#	.cspell-wordlist.txt
- podspec: stop wrapping `$(PODS_TARGET_SRCROOT)` paths in File.expand_path
  (it baked a malformed `<dir>/$(PODS_TARGET_SRCROOT)/...` into the linker
  flags); use the literal Xcode build variable like HEADER_SEARCH_PATHS does.
- download-libs.js: only send GITHUB_TOKEN to github.com hosts (not to the
  githubusercontent.com presigned redirect target).
- third-party/.gitignore: ignore the downloaded executorch.jar.
- RnExecutorch.cpp: drop the redundant comment on the opencv include guard.
@msluszniak

Copy link
Copy Markdown
Member Author
  • Also, did you check the example app size with and without the split and how much it decreases it?

  • Would you be able to check that MLX backend gets correctly bundled for the computer-vision app (I don't have an iOS physical device available)?

  • Do we have some custom script for vendoring the executorch headers, because the headers downloaded using the split script do not match the ones copied directly from executorch repo used in rnet-poc, e.g. all llm runner helpers are missing?

  1. Haven't check yet, will do this in a minute.
  2. Unfortunately, neither do I, so we need to defer this check till Monday.
  3. That's my fault. There is no such script and that's the reason why I missed headers. I will add such script and update published assets.

The headers.tar.gz set was assembled by an ad-hoc copy of the executorch
CMake install include tree, which is incomplete: it omits the source-only
headers (extension/llm/{runner,custom_ops,apple,sampler}) that the rewrite's
LLM/multimodal tasks compile against directly, and the codegen'd
kernels/*/Functions.h.

vendor-headers.sh assembles the full, reproducible set from its four real
sources — executorch C++ source headers, build-generated/installed headers,
c10/torch from the xcframework, and opencv2 — laid out to satisfy both the
nested tokenizer include paths (CMakeLists) and the root -I. The result is a
superset of both the current RNE main header set and the CV PoC.

package-release-artifacts.sh now hard-fails if third-party/include is missing
the runner headers, and the native-libraries doc documents the step.
@msluszniak

Copy link
Copy Markdown
Member Author

@barhanc I added mentioned script, now working on memory comparison for cv demo app.

@msluszniak

msluszniak commented Jun 26, 2026

Copy link
Copy Markdown
Member Author

App-size impact of the backend split

Measured on the computer-vision demo (release APK, arm64-v8a). iOS figures are artifact-derived (no IPA built yet). Native .so are stored uncompressed, so they land in the APK at full size.

CV app — split vs all-backends

vs complete bundle vs RNE-only files
Android (measured) 78.3 → 68.3 MB (−10.0 MB, −13%) 25.7 → 15.7 MB (−10.0 MB, −39%)
iOS (estimated) ~0 ~0

iOS is ~0 here because the CV app uses every iOS backend (xnnpack + coreml + mlx via keypointDetection). Android RNE-only footprint = libexecutorch core 11.3 + libxnnpack… 2.6 + libRnExecutorch (+ statically-linked opencv) 1.9, plus Vulkan 10.0 in the no-split build.

What the split drops, per backend (the cross-app value)

Component Platform Dropped Notes
Vulkan Android 10.0 MB dynamic .so, full size
MLX iOS ~13 MB .a + 2.7 MB mlx.metallib -force_load → not stripped; metallib is a non-strippable resource
CoreML iOS ~1.8 MB .a -force_load
XNNPACK both 2.6 / 7 MB almost always needed
opencv both ~1.5 MB effective static + dead-stripped, so ≪ its 13 MB of .a
phonemis both ~1.5–2.5 MB (once Kokoro TTS lands) source-compiled into the lib; the 22 MB espeak data/ is stripped from the npm package

msluszniak added a commit that referenced this pull request Jun 26, 2026
A conservative, high-signal clang-tidy config plus a runner and a (dormant)
CI workflow.

- .clang-tidy: bugprone-* / performance-* / clang-analyzer-* (minus the noisy
  easily-swappable-parameters and enum-size), analyzing only our own headers.
- scripts/clang-tidy.sh + `lint:cpp`: run clang-tidy over cpp/ using
  compile_flags.txt, failing on any finding.
- .github/workflows/clang-tidy.yml: gated behind ENABLE_CLANG_TIDY; provisions
  headers via the on-demand download flow (download-libs.js, #1283) since
  clang-tidy needs the ExecuTorch headers to parse the sources.
- dtype.cpp: NOLINT the intentional identical-size switch branches.
msluszniak added a commit that referenced this pull request Jun 26, 2026
A conservative, high-signal clang-tidy config plus a runner and a (dormant)
CI workflow.

- .clang-tidy: bugprone-* / performance-* / clang-analyzer-* (minus the noisy
  easily-swappable-parameters and enum-size), analyzing only our own headers.
- scripts/clang-tidy.sh + `lint:cpp`: run clang-tidy over cpp/ using
  compile_flags.txt, failing on any finding.
- .github/workflows/clang-tidy.yml: gated behind ENABLE_CLANG_TIDY; provisions
  headers via the on-demand download flow (download-libs.js, #1283) since
  clang-tidy needs the ExecuTorch headers to parse the sources.
- dtype.cpp: NOLINT the intentional identical-size switch branches.
Provisions only headers.tar.gz (no per-target native libs) when
RNET_HEADERS_ONLY is set — the integration point for clang-tidy / IDE
tooling that needs include paths but never links or runs the binaries.
The build config is still written. Also documents the existing
RNET_NO_X86_64 opt-out in the README/env-var lists.
Comment thread packages/react-native-executorch/react-native-executorch.podspec
…T_SRCROOT

The backend force_load entries land in OTHER_LDFLAGS, applied at the
consuming app target's link step, where $(PODS_TARGET_SRCROOT) (a
pod-scoped variable) is undefined and expands to empty — producing
/third-party/ios/.../lib*.a and a "build input files cannot be found"
error (reported on the iOS simulator). Use an absolute path baked at
podspec-eval time. HEADER_SEARCH_PATHS keep $(PODS_TARGET_SRCROOT): those
are the pod's own compile settings, where it resolves correctly.
@msluszniak msluszniak requested a review from barhanc June 29, 2026 13:05
@barhanc

barhanc commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Tested iOS on device and the backends work.

The artifact build branch on software-mansion-labs/executorch is now
rne-split-build (was @ms/separate-backends); update every reference in the
docs, vendor-headers.sh, and third-party/README.
…native-libs

# Conflicts:
#	.cspell-wordlist.txt
…atures

The demo app now has detection (#1284) and instance-segmentation (#1289)
screens (useObjectDetector / useInstanceSegmenter); declare them so the
feature list matches the hooks the app actually uses.
@msluszniak msluszniak merged commit 95acdba into rne-rewrite Jun 29, 2026
2 checks passed
@msluszniak msluszniak deleted the @ms/on-demand-native-libs branch June 29, 2026 15:00
msluszniak added a commit that referenced this pull request Jun 29, 2026
A conservative, high-signal clang-tidy config plus a runner and a (dormant)
CI workflow.

- .clang-tidy: bugprone-* / performance-* / clang-analyzer-* (minus the noisy
  easily-swappable-parameters and enum-size), analyzing only our own headers.
- scripts/clang-tidy.sh + `lint:cpp`: run clang-tidy over cpp/ using
  compile_flags.txt, failing on any finding.
- .github/workflows/clang-tidy.yml: gated behind ENABLE_CLANG_TIDY; provisions
  headers via the on-demand download flow (download-libs.js, #1283) since
  clang-tidy needs the ExecuTorch headers to parse the sources.
- dtype.cpp: NOLINT the intentional identical-size switch branches.
msluszniak added a commit that referenced this pull request Jun 29, 2026
## Description

Adds clang-tidy static analysis for the core package's C/C++ sources.
Second step of #1271 (clangd setup landed in #1285). Rebased onto
`rne-rewrite`, so this is clang-tidy-only.

- `.clang-tidy` — a high-signal check set enabled group by group (one
commit each): `bugprone-*`, `performance-*`, `clang-analyzer-*`,
`cppcoreguidelines-*`, `google-*`, `llvm-*`, `misc-*`, `modernize-*`,
`readability-*`. Each group's narrow exclusion list (raw-buffer pointer
math, `reinterpret_cast`, `#pragma once`, JSI multiple-inheritance,
opinionated/noisy checks) is documented inline with rationale. Analyzes
only this package's own `cpp/` headers; third-party is `-isystem`.
- `scripts/clang-tidy.sh` + the `lint:cpp` package script — run
clang-tidy over `cpp/` using `compile_flags.txt` (the same database
clangd uses), failing on any finding.
- `.github/workflows/clang-tidy.yml` — CI gate, **dormant** behind the
`ENABLE_CLANG_TIDY` repo variable. It provisions headers through the
on-demand download flow (`download-libs.js`, #1283) rather than a stub,
since clang-tidy needs the ExecuTorch headers to parse the sources.

The fixes each group surfaced are split across the per-group commits
(scoped `DType` enum, member-init lists, `explicit` ctor,
`const`-correctness cleanup, `use-auto`/`emplace`/`cmp_*`
modernizations, `NOLINT`s for the intentional cases).

Depends on #1283 for `download-libs.js`; enable the workflow once that
has merged and a release carrying `headers.tar.gz` exists. The
provisioning step sets `RNET_HEADERS_ONLY=1` (clang-tidy needs only
headers) — see PR thread re: honoring that flag in `download-libs.js`.

Verified locally (Homebrew LLVM clang-tidy): `lint:cpp` is green on all
sources and exits non-zero on an injected finding.

### Introduces a breaking change?

- [ ] Yes
- [x] No

### Type of change

- [ ] Bug fix (change which fixes an issue)
- [ ] New feature (change which adds functionality)
- [ ] Documentation update (improves or adds clarity to existing
documentation)
- [x] Other (chores, tests, code style improvements etc.)

### Tested on

- [ ] iOS
- [ ] Android

### Testing instructions

1. Provision `packages/react-native-executorch/third-party/include` and
run `yarn install`.
2. From the repo root: `CLANG_TIDY=$(brew --prefix llvm)/bin/clang-tidy
yarn workspace react-native-executorch lint:cpp` — expect 0 findings.
3. Confirm the gate has teeth: introduce a bug in a `cpp/` file, e.g.
`double r = a / b;` (ints) for `bugprone-integer-division`, and re-run —
expect a non-zero exit. Revert afterwards.

### Related issues

#1271

### Checklist

- [x] I have performed a self-review of my code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have updated the documentation accordingly
- [x] My changes generate no new warnings
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement PRs or issues focused on improvements in the current codebase refactoring

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants