react-native-executorch 0.10.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -53,6 +53,44 @@ fun rneBuildConfig(): Map<*, *> {
53
53
  val rneConfig = rneBuildConfig()
54
54
  fun rneFlag(key: String): String = if (rneConfig[key] != false) "ON" else "OFF"
55
55
 
56
+ /**
57
+ * The prebuilt ExecuTorch runtime is not in the npm tarball - `package.json`
58
+ * excludes it and `scripts/download-libs.js` fetches it from the matching
59
+ * GitHub release in a postinstall hook. When a package manager skips that hook
60
+ * the install still looks clean and the failure surfaces much later, out of
61
+ * CMake, blamed on a missing library rather than on the install. Fail here with
62
+ * the fix instead.
63
+ */
64
+ fun requireNativeArtifacts() {
65
+ val libsDir = file("../third-party/android/libs/executorch")
66
+ val present = libsDir.listFiles()
67
+ ?.filter { it.isDirectory && it.resolve("libexecutorch.so").exists() }
68
+ .orEmpty()
69
+ if (present.isNotEmpty()) return
70
+
71
+ throw GradleException(
72
+ """
73
+ react-native-executorch is missing its native artifacts:
74
+
75
+ ${libsDir.absolutePath}
76
+
77
+ They are downloaded by this package's postinstall hook, which your
78
+ package manager did not run. pnpm 10 and later block dependency build
79
+ scripts by default ("Ignored build scripts"), and so do
80
+ `--ignore-scripts` and `npm ci --ignore-scripts`. Re-run the hook:
81
+
82
+ pnpm approve-builds react-native-executorch # pnpm
83
+ npm rebuild react-native-executorch # npm
84
+ node node_modules/react-native-executorch/scripts/download-libs.js
85
+
86
+ If you provision the libraries yourself, put them under
87
+ third-party/android/libs/executorch/<abi>/ before building.
88
+ """.trimIndent()
89
+ )
90
+ }
91
+
92
+ requireNativeArtifacts()
93
+
56
94
  /**
57
95
  * ExecuTorch only supports these ABIs. Honor the app's `reactNativeArchitectures`
58
96
  * (e.g. Expo passes `-PreactNativeArchitectures=arm64-v8a` for device builds) so
@@ -70,7 +108,12 @@ android {
70
108
  compileSdk = (getExtOrDefault("compileSdkVersion", 34) as Number).toInt()
71
109
 
72
110
  defaultConfig {
73
- minSdk = (getExtOrDefault("minSdkVersion", 21) as Number).toInt()
111
+ // The prebuilt ExecuTorch runtime is compiled against Android API 26 -
112
+ // `.note.android.ident` in libexecutorch.so, libxnnpack_executorch_backend.so
113
+ // and libvulkan_executorch_backend.so all read 26 - so an app below that
114
+ // ships a library its linker is not guaranteed to be able to load. The
115
+ // default was 21, which let such a build through to fail on the device.
116
+ minSdk = (getExtOrDefault("minSdkVersion", 26) as Number).toInt()
74
117
  targetSdk = (getExtOrDefault("targetSdkVersion", 34) as Number).toInt()
75
118
  consumerProguardFiles("consumer-proguard-rules.pro")
76
119
 
@@ -12,7 +12,7 @@ const DOWNLOAD_EVENT_ENDPOINT = 'https://ai.swmansion.com/telemetry/downloads/ap
12
12
  // self-referencing import would need package `exports` support in the consuming
13
13
  // bundler to load at all. A wrong value here is analytics noise; a failed
14
14
  // import would break the bundle.
15
- const LIB_VERSION = '0.10.0';
15
+ const LIB_VERSION = '0.10.1';
16
16
 
17
17
  // Anonymous analytics are on by default; apps opt out via setTelemetryEnabled.
18
18
  let telemetryEnabled = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-executorch",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "nativeLibsVersion": "0.10.0",
5
5
  "description": "An easy way to run AI models in React Native with ExecuTorch",
6
6
  "main": "./lib/module/index.js",
@@ -142,7 +142,7 @@
142
142
  "react": "*",
143
143
  "react-native": "*",
144
144
  "react-native-blob-util": "^0.24.0",
145
- "react-native-worklets": "^0.10.0"
145
+ "react-native-worklets": ">=0.10.0 <0.13.0"
146
146
  },
147
147
  "peerDependenciesMeta": {
148
148
  "@kesha-antonov/react-native-background-downloader": {
@@ -1,3 +1,4 @@
1
+ require "fileutils"
1
2
  require "json"
2
3
 
3
4
  package = JSON.parse(File.read(File.join(__dir__, "package.json")))
@@ -6,19 +7,59 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
6
7
  # Falls back to all features enabled if the file doesn't exist (e.g. a fresh
7
8
  # checkout where the native libs were provisioned manually).
8
9
  rne_build_config_path = File.join(__dir__, "rne-build-config.json")
9
- if File.exist?(rne_build_config_path)
10
- rne_build_config = JSON.parse(File.read(rne_build_config_path))
11
- enable_opencv = rne_build_config["enableOpencv"] != false
12
- enable_phonemis = rne_build_config["enablePhonemis"] != false
13
- enable_xnnpack = rne_build_config["enableXnnpack"] != false
14
- enable_coreml = rne_build_config["enableCoreml"] != false
15
- enable_mlx = rne_build_config["enableMlx"] != false
16
- else
17
- enable_opencv = true
18
- enable_phonemis = true
19
- enable_xnnpack = true
20
- enable_coreml = true
21
- enable_mlx = true
10
+ rne_build_config =
11
+ File.exist?(rne_build_config_path) ? JSON.parse(File.read(rne_build_config_path)) : {}
12
+
13
+ # Every flag reads `!= false`, so an absent file and an absent key both mean
14
+ # enabled. Keep the hash rather than branching on the file: `opencvPod` below
15
+ # is read from it too, and a nil `rne_build_config` there is a NoMethodError.
16
+ enable_opencv = rne_build_config["enableOpencv"] != false
17
+ enable_phonemis = rne_build_config["enablePhonemis"] != false
18
+ enable_xnnpack = rne_build_config["enableXnnpack"] != false
19
+ enable_coreml = rne_build_config["enableCoreml"] != false
20
+ enable_mlx = rne_build_config["enableMlx"] != false
21
+
22
+ # The native artifacts are not in the npm tarball - `package.json` excludes
23
+ # them and `scripts/download-libs.js` fetches them from the matching GitHub
24
+ # release in a postinstall hook. When a package manager skips that hook the
25
+ # install still looks clean, and so does `pod install`: CocoaPods never checks
26
+ # that a vendored framework exists. The build then fails minutes later with
27
+ #
28
+ # error: Build input files cannot be found:
29
+ # '.../XnnpackBackend.xcframework/ios-arm64-simulator/libXnnpackBackend.a'
30
+ #
31
+ # which names neither the cause nor the fix. Fail here instead, while the user
32
+ # is still looking at the install that caused it.
33
+ required_artifacts = { "ExecutorchLib.xcframework" => true }
34
+ required_artifacts["XnnpackBackend.xcframework"] = enable_xnnpack
35
+ required_artifacts["CoreMLBackend.xcframework"] = enable_coreml
36
+ required_artifacts["MLXBackend.xcframework"] = enable_mlx
37
+
38
+ missing = required_artifacts
39
+ .select { |_, required| required }
40
+ .keys
41
+ .map { |name| File.join(__dir__, "third-party/ios", name) }
42
+ .reject { |path| File.directory?(path) }
43
+
44
+ unless missing.empty?
45
+ # Pod::Informative renders as a plain `[!]` message rather than a backtrace.
46
+ raise(defined?(Pod::Informative) ? Pod::Informative : StandardError, <<~MESSAGE)
47
+ react-native-executorch is missing its native artifacts:
48
+
49
+ #{missing.map { |path| " #{path}" }.join("\n")}
50
+
51
+ They are downloaded by this package's postinstall hook, which your package
52
+ manager did not run. pnpm 10 and later block dependency build scripts by
53
+ default ("Ignored build scripts"), and so do `--ignore-scripts` and
54
+ `npm ci --ignore-scripts`. Re-run the hook, then `pod install` again:
55
+
56
+ pnpm approve-builds react-native-executorch # pnpm
57
+ npm rebuild react-native-executorch # npm
58
+ node node_modules/react-native-executorch/scripts/download-libs.js
59
+
60
+ If you provision the libraries yourself, put them under
61
+ third-party/ios/ before installing pods.
62
+ MESSAGE
22
63
  end
23
64
 
24
65
  Pod::Spec.new do |s|
@@ -97,6 +138,34 @@ Pod::Spec.new do |s|
97
138
  exclude_files += phonemis_source_files unless enable_phonemis
98
139
  s.exclude_files = exclude_files
99
140
 
141
+ # --- Public headers ---
142
+ # `use_frameworks!` - set directly for Firebase, or through
143
+ # expo-build-properties' `useFrameworks: "static"` - makes CocoaPods build
144
+ # this pod as a framework, and Xcode's Headers build phase copies every
145
+ # *public* header into one flat `Headers/` directory. With no
146
+ # `public_header_files` every header in `source_files` is public, and the
147
+ # tree has 21 basenames that occur more than once (`Types.h` in eleven task
148
+ # directories, `constants.h` in thirteen phonemis ones), so the build fails
149
+ # at planning with `Multiple commands produce .../Headers/Types.h` before a
150
+ # single file compiles. See discussion #203.
151
+ #
152
+ # Only the Objective-C entry points have to be visible to the app. The C++
153
+ # headers are reached through the HEADER_SEARCH_PATHS below, so narrowing the
154
+ # public set costs nothing, and `__tests__/api/podspecPublicHeaders.test.ts`
155
+ # keeps it collision-free.
156
+ public_header_files = [
157
+ "ios/**/*.h",
158
+ ]
159
+ # ==============================================================================
160
+ # LEGACY SUPPORT: include the legacy entry point
161
+ # (Remove when react-native-executorch/legacy is dropped)
162
+ # ==============================================================================
163
+ public_header_files += [
164
+ "legacy/ios/**/*.h",
165
+ ]
166
+ # ==============================================================================
167
+ s.public_header_files = public_header_files
168
+
100
169
  # --- Preprocessor flags ---
101
170
  extra_compiler_flags = []
102
171
  extra_compiler_flags << "-DRNE_ENABLE_OPENCV" if enable_opencv
@@ -143,6 +212,64 @@ Pod::Spec.new do |s|
143
212
  'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'x86_64',
144
213
  }
145
214
 
215
+ # iOS OpenCV is provided by a CocoaPod (not a downloaded tarball), normally
216
+ # our own opencv-rne.
217
+ #
218
+ # An app can already carry OpenCV through another library, and CocoaPods
219
+ # refuses to install two vendored frameworks with the same name:
220
+ #
221
+ # [!] The 'Pods-YourApp' target has frameworks with conflicting names:
222
+ # opencv2.xcframework
223
+ #
224
+ # react-native-fast-opencv is the one this happens with, and wanting both is
225
+ # reasonable: our inference API with their image transformations. So when it
226
+ # is installed alongside us we depend on the pod it vendors instead of our
227
+ # own, which leaves exactly one opencv2 in the project. We only use
228
+ # `opencv2/core.hpp` and `opencv2/imgproc.hpp`, so any OpenCV 4.x build
229
+ # serves. Set "opencvPod" in the package.json config block to override the
230
+ # choice in either direction.
231
+ external_opencv = false
232
+ if enable_opencv
233
+ detected_opencv_pod =
234
+ if Dir.exist?(File.join(__dir__, "..", "react-native-fast-opencv"))
235
+ "FastOpenCV-iOS"
236
+ else
237
+ "opencv-rne"
238
+ end
239
+ opencv_pod = rne_build_config["opencvPod"] || detected_opencv_pod
240
+
241
+ if opencv_pod == "opencv-rne"
242
+ s.dependency "opencv-rne", "~> 4.11.0"
243
+ else
244
+ Pod::UI.puts "[react-native-executorch] using #{opencv_pod} for OpenCV " \
245
+ "instead of opencv-rne, so the project holds one opencv2" if defined?(Pod::UI)
246
+ s.dependency opencv_pod
247
+ external_opencv = true
248
+ end
249
+ end
250
+
251
+ # Our own OpenCV headers ship under third-party/include and are newer than
252
+ # what another OpenCV pod vendors: react-native-fast-opencv carries 4.9, and
253
+ # compiling against ours while linking against theirs fails at link time on
254
+ # any signature that moved since (cvtColor gained an AlgorithmHint parameter
255
+ # in 4.10). Whoever provides the binary has to provide the headers, so with
256
+ # an external OpenCV the include root becomes a mirror of ours with opencv2
257
+ # left out, and `#include <opencv2/...>` resolves through their framework.
258
+ mirror_without_opencv = lambda do
259
+ source = File.join(__dir__, "third-party/include")
260
+ mirror = File.join(__dir__, "third-party/include-external-opencv")
261
+ FileUtils.rm_rf(mirror)
262
+ FileUtils.mkdir_p(mirror)
263
+ Dir.children(source).each do |entry|
264
+ next if entry == "opencv2"
265
+ FileUtils.ln_s(File.join(source, entry), File.join(mirror, entry))
266
+ end
267
+ "third-party/include-external-opencv"
268
+ end
269
+
270
+ third_party_include =
271
+ external_opencv ? mirror_without_opencv.call : "third-party/include"
272
+
146
273
  s.pod_target_xcconfig = {
147
274
  "USE_HEADERMAP" => "YES",
148
275
  "CLANG_CXX_LANGUAGE_STANDARD" => "c++20",
@@ -158,7 +285,7 @@ Pod::Spec.new do |s|
158
285
  # ==============================================================================
159
286
  "\"$(PODS_TARGET_SRCROOT)/legacy/cpp\"",
160
287
  # ==============================================================================
161
- "\"$(PODS_TARGET_SRCROOT)/third-party/include\"",
288
+ "\"$(PODS_TARGET_SRCROOT)/#{third_party_include}\"",
162
289
  "\"$(PODS_TARGET_SRCROOT)/third-party/include/cpuinfo\"",
163
290
  "\"$(PODS_TARGET_SRCROOT)/third-party/include/pthreadpool\"",
164
291
  "\"$(PODS_TARGET_SRCROOT)/third-party/include/executorch/extension/llm/tokenizers/include\"",
@@ -189,13 +316,23 @@ Pod::Spec.new do |s|
189
316
  # resource path. `s.ios.resource` achieves that via CocoaPods' resource copy.
190
317
  s.ios.resource = "third-party/ios/libs/executorch/mlx.metallib" if enable_mlx
191
318
 
319
+ # An app that turns on `use_frameworks!` without naming a linkage gets the
320
+ # default, dynamic - which is what Firebase's own setup instructions show.
321
+ # CocoaPods then refuses to install at all, because a dynamic framework may
322
+ # not carry statically linked binaries and opencv-rne vendors one:
323
+ #
324
+ # [!] The 'Pods-YourApp' target has transitive dependencies that include
325
+ # statically linked binaries: (.../opencv-rne/opencv2.xcframework)
326
+ #
327
+ # Declaring the pod a static framework resolves that without the app having
328
+ # to spell out `:linkage => :static`, and is inert when the pod is built as a
329
+ # static library, which is what happens with no `use_frameworks!` at all.
330
+ s.static_framework = true
331
+
192
332
  # Backend xcframeworks are linked via force_load in OTHER_LDFLAGS (needed to
193
333
  # preserve __attribute__((constructor)) backend registrations). Only
194
334
  # ExecutorchLib goes in vendored_frameworks to avoid duplicate symbol errors.
195
335
  s.ios.vendored_frameworks = ["third-party/ios/ExecutorchLib.xcframework"]
196
336
 
197
- # iOS OpenCV is provided by the opencv-rne CocoaPod (not a downloaded tarball).
198
- s.dependency "opencv-rne", "~> 4.11.0" if enable_opencv
199
-
200
337
  install_modules_dependencies(s)
201
338
  end
@@ -172,32 +172,67 @@ const FEATURE_MAP = {
172
172
  tokenizer: { backends: [], libs: [] },
173
173
  };
174
174
 
175
- function readUserConfig() {
176
- const allOn = () => ({ backends: [...ALL_BACKENDS], libs: [...ALL_LIBS] });
175
+ /**
176
+ * The `react-native-executorch` block, and the package.json it came from.
177
+ *
178
+ * Two places are tried, in order:
179
+ *
180
+ * 1. INIT_CWD - where the install was invoked. For a single-app project that
181
+ * is the app itself.
182
+ * 2. Each package.json above this package. In a workspace, `yarn install` is
183
+ * run at the root, so INIT_CWD points there and a block in
184
+ * `apps/mobile/package.json` would never be seen. When the app keeps its
185
+ * own node_modules (pnpm, nohoist), walking up from here lands on the app.
186
+ *
187
+ * A hoisted workspace resolves to the root either way, which is where the block
188
+ * has to live in that layout - there is no way to tell which of several apps a
189
+ * root install was meant for.
190
+ * @returns The block and the path of the package.json holding it, or both
191
+ * `undefined` when no manifest declares one.
192
+ */
193
+ function findUserConfig() {
194
+ const candidates = [];
195
+ const initCwd = process.env.INIT_CWD || process.env.npm_config_local_prefix;
196
+ if (initCwd) candidates.push(initCwd);
197
+
198
+ // `__dirname` is <somewhere>/node_modules/react-native-executorch/scripts.
199
+ let dir = path.resolve(__dirname, '..', '..', '..');
200
+ for (let i = 0; i < 6; i++) {
201
+ if (!candidates.includes(dir)) candidates.push(dir);
202
+ const parent = path.dirname(dir);
203
+ if (parent === dir) break;
204
+ dir = parent;
205
+ }
177
206
 
178
- // npm/yarn set INIT_CWD to the directory where install was invoked (project root)
179
- const projectRoot = process.env.INIT_CWD || process.env.npm_config_local_prefix;
180
- if (!projectRoot) {
181
- console.warn(
182
- '[react-native-executorch] Could not determine project root, enabling all backends + libs.'
183
- );
184
- return allOn();
207
+ for (const root of candidates) {
208
+ const manifest = path.join(root, 'package.json');
209
+ let parsed;
210
+ try {
211
+ parsed = JSON.parse(fs.readFileSync(manifest, 'utf8'));
212
+ } catch {
213
+ continue;
214
+ }
215
+ if (parsed['react-native-executorch'] !== undefined) {
216
+ return { config: parsed['react-native-executorch'], manifest };
217
+ }
185
218
  }
219
+ return { config: undefined, manifest: undefined };
220
+ }
186
221
 
187
- let rneConfig;
188
- try {
189
- const userPackageJson = JSON.parse(
190
- fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8')
191
- );
192
- rneConfig = userPackageJson['react-native-executorch'];
193
- } catch {
194
- console.warn(
195
- '[react-native-executorch] Could not read app package.json, enabling all backends + libs.'
222
+ function readUserConfig() {
223
+ const allOn = () => ({ backends: [...ALL_BACKENDS], libs: [...ALL_LIBS], opencvPod: undefined });
224
+
225
+ const { config: rneConfig, manifest } = findUserConfig();
226
+
227
+ if (rneConfig === undefined) {
228
+ console.log(
229
+ '[react-native-executorch] No `react-native-executorch` block found; enabling all backends + libs.'
196
230
  );
197
231
  return allOn();
198
232
  }
199
-
200
- if (rneConfig === undefined) return allOn();
233
+ // Which manifest won matters when it is not the one the user edited - the
234
+ // usual monorepo surprise.
235
+ console.log(`[react-native-executorch] Read build config from ${manifest}`);
201
236
 
202
237
  if (rneConfig.extras !== undefined) {
203
238
  throw new Error(
@@ -236,10 +271,10 @@ function readUserConfig() {
236
271
  }
237
272
  }
238
273
 
239
- return { backends: [...backends], libs: [...libs] };
274
+ return { backends: [...backends], libs: [...libs], opencvPod: rneConfig.opencvPod };
240
275
  }
241
276
 
242
- function writeBuildConfig({ backends, libs }) {
277
+ function writeBuildConfig({ backends, libs, opencvPod }) {
243
278
  const config = {
244
279
  enableOpencv: libs.includes('opencv'),
245
280
  enablePhonemis: libs.includes('phonemis'),
@@ -248,6 +283,10 @@ function writeBuildConfig({ backends, libs }) {
248
283
  enableMlx: backends.includes('mlx'),
249
284
  enableVulkan: backends.includes('vulkan'),
250
285
  };
286
+ // Which pod provides opencv2 on iOS. Only set when the app asks for a
287
+ // specific one; otherwise the podspec picks, preferring an OpenCV the app
288
+ // already carries so two frameworks named opencv2 never meet.
289
+ if (opencvPod !== undefined) config.opencvPod = opencvPod;
251
290
  fs.writeFileSync(
252
291
  path.join(PACKAGE_ROOT, 'rne-build-config.json'),
253
292
  JSON.stringify(config, null, 2)
@@ -497,4 +536,4 @@ if (require.main === module) {
497
536
  });
498
537
  }
499
538
 
500
- module.exports = { ALL_BACKENDS, ALL_LIBS, FEATURE_MAP };
539
+ module.exports = { ALL_BACKENDS, ALL_LIBS, FEATURE_MAP, findUserConfig, readUserConfig };
@@ -10,7 +10,7 @@ const DOWNLOAD_EVENT_ENDPOINT = 'https://ai.swmansion.com/telemetry/downloads/ap
10
10
  // self-referencing import would need package `exports` support in the consuming
11
11
  // bundler to load at all. A wrong value here is analytics noise; a failed
12
12
  // import would break the bundle.
13
- const LIB_VERSION = '0.10.0';
13
+ const LIB_VERSION = '0.10.1';
14
14
 
15
15
  // Anonymous analytics are on by default; apps opt out via setTelemetryEnabled.
16
16
  let telemetryEnabled = true;