expo-modules-autolinking 55.0.18 → 55.0.20
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.
- package/CHANGELOG.md +15 -0
- package/external-configs/ios/README.md +8 -0
- package/package.json +2 -2
- package/scripts/ios/precompiled_modules.rb +315 -80
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,21 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.20 — 2026-05-05
|
|
14
|
+
|
|
15
|
+
_This version does not introduce any user-facing changes._
|
|
16
|
+
|
|
17
|
+
## 55.0.19 — 2026-04-28
|
|
18
|
+
|
|
19
|
+
### 🐛 Bug fixes
|
|
20
|
+
|
|
21
|
+
- [iOS] Disable precompiled modules with a clear warning when `EXPO_USE_PRECOMPILED_MODULES=1` is set without `RCT_USE_PREBUILT_RNCORE=1` ([#45381](https://github.com/expo/expo/pull/45381) by [@chrfalch](https://github.com/chrfalch))
|
|
22
|
+
- [iOS] Fall back to source build when a remote precompiled XCFramework download returns 404. ([#45240](https://github.com/expo/expo/pull/45240) by [@chrfalch](https://github.com/chrfalch))
|
|
23
|
+
- [iOS] Resolve npm-bundled precompiled XCFrameworks for nested Expo packages like `expo-modules-core` during `pod install`. ([#45166](https://github.com/expo/expo/pull/45166) by [@chrfalch](https://github.com/chrfalch))
|
|
24
|
+
- [iOS] Build precompiled Expo modules from source when a required upstream Expo dependency is unavailable as prebuilt. ([#45160](https://github.com/expo/expo/pull/45160) by [@chrfalch](https://github.com/chrfalch))
|
|
25
|
+
- [iOS] Add support for optionally downloading external precompiled XCFramework tarballs from during `pod install`. ([#45067](https://github.com/expo/expo/pull/45067) by [@chrfalch](https://github.com/chrfalch))
|
|
26
|
+
- [iOS] Resolve 3rd-party prebuilt xcframework packages via `@react-native-community/cli` autolinking output instead of guessing `node_modules/<pkg>`, fixing pnpm non-hoisted layouts, transitive native deps, yarn resolutions/PnP, and aliased specifiers ([#45004](https://github.com/expo/expo/pull/45004) by [@chrfalch](https://github.com/chrfalch))
|
|
27
|
+
|
|
13
28
|
## 55.0.18 — 2026-04-21
|
|
14
29
|
|
|
15
30
|
### 🐛 Bug fixes
|
|
@@ -36,6 +36,14 @@ Configs live in `packages/expo-modules-autolinking/external-configs/ios/<package
|
|
|
36
36
|
- **Config location**: `packages/expo-modules-autolinking/external-configs/ios/<package-name>/spm.config.json`
|
|
37
37
|
- **Source location**: `node_modules/<package-name>/` (resolved at build time)
|
|
38
38
|
- **Output location**: `packages/precompile/.build/<package-name>/output/<flavor>/xcframeworks/`
|
|
39
|
+
- **Remote fallback**: external package prebuilds can download missing tarballs from a remote host when `EXPO_PRECOMPILED_MODULES_BASE_URL` is set.
|
|
40
|
+
|
|
41
|
+
### Remote Artifact Downloads
|
|
42
|
+
|
|
43
|
+
Remote downloads are opt-in via `EXPO_PRECOMPILED_MODULES_BASE_URL`.
|
|
44
|
+
|
|
45
|
+
- Supports `http://` and `https://` URLs.
|
|
46
|
+
- Final download path: `<baseUrl>/<npm-package>/output/<packageVersion>/<reactNativeVersion>/<hermesVersion>/<flavor>/xcframeworks/<Product>.tar.gz` when versions are known, otherwise `<baseUrl>/<npm-package>/output/<flavor>/xcframeworks/<Product>.tar.gz`
|
|
39
47
|
|
|
40
48
|
### The SPMPackageSource Interface
|
|
41
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-autolinking",
|
|
3
|
-
"version": "55.0.
|
|
3
|
+
"version": "55.0.20",
|
|
4
4
|
"description": "Scripts that autolink Expo modules.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -43,5 +43,5 @@
|
|
|
43
43
|
"chalk": "^4.1.0",
|
|
44
44
|
"commander": "^7.2.0"
|
|
45
45
|
},
|
|
46
|
-
"gitHead": "
|
|
46
|
+
"gitHead": "17007db30c487d21b82548365b84180a73af25ff"
|
|
47
47
|
}
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
|
|
29
29
|
require 'fileutils'
|
|
30
30
|
require 'json'
|
|
31
|
+
require 'net/http'
|
|
32
|
+
require 'set'
|
|
31
33
|
require 'uri'
|
|
32
34
|
|
|
33
35
|
module Expo
|
|
@@ -41,6 +43,9 @@ module Expo
|
|
|
41
43
|
# Environment variable for custom precompiled modules base path
|
|
42
44
|
MODULES_PATH_ENV_VAR = 'EXPO_PRECOMPILED_MODULES_PATH'.freeze
|
|
43
45
|
|
|
46
|
+
# Environment variable for a shared remote base URL used by external prebuilt packages.
|
|
47
|
+
EXTERNAL_MODULES_BASE_URL_ENV_VAR = 'EXPO_PRECOMPILED_MODULES_BASE_URL'.freeze
|
|
48
|
+
|
|
44
49
|
# Subdirectory within each pod dir for tarballs and build state
|
|
45
50
|
ARTIFACTS_DIR_NAME = 'artifacts'.freeze
|
|
46
51
|
|
|
@@ -56,6 +61,10 @@ module Expo
|
|
|
56
61
|
# Regex to strip `framework module React { ... }` from modulemaps
|
|
57
62
|
FRAMEWORK_MODULE_REACT_REGEX = /framework module React \{.*?\n\}\s*/m
|
|
58
63
|
|
|
64
|
+
# ExpoModulesJSI is always provided as an xcframework by its own podspec/npm package,
|
|
65
|
+
# so it is not resolved through the Expo precompiled tarball pipeline.
|
|
66
|
+
CUSTOM_XCFRAMEWORK_DEPENDENCIES = %w[ExpoModulesJSI].freeze
|
|
67
|
+
|
|
59
68
|
# Module-level caches (initialized lazily)
|
|
60
69
|
@pod_lookup_map = nil
|
|
61
70
|
@repo_root = nil
|
|
@@ -65,6 +74,8 @@ module Expo
|
|
|
65
74
|
@hermes_version = nil
|
|
66
75
|
@claimed_vendored_frameworks = nil # Set<String> — xcframework names already claimed by a prebuilt pod
|
|
67
76
|
@framework_owner_map = nil # Hash: framework_name -> owning_pod_name
|
|
77
|
+
@failed_remote_downloads = Set.new
|
|
78
|
+
@warned_no_prebuilt_react = false
|
|
68
79
|
|
|
69
80
|
class << self
|
|
70
81
|
# Returns the build flavor (debug/release) for precompiled modules.
|
|
@@ -81,9 +92,25 @@ module Expo
|
|
|
81
92
|
ENV[MODULES_PATH_ENV_VAR]
|
|
82
93
|
end
|
|
83
94
|
|
|
84
|
-
# Returns
|
|
95
|
+
# Returns the shared base URL for remote external prebuilt artifacts, if set.
|
|
96
|
+
def external_modules_base_url
|
|
97
|
+
ENV[EXTERNAL_MODULES_BASE_URL_ENV_VAR]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Returns true if precompiled modules are enabled via environment variable.
|
|
101
|
+
# Precompiled module xcframeworks are linked against the prebuilt
|
|
102
|
+
# React.xcframework, so they also require RCT_USE_PREBUILT_RNCORE=1. If
|
|
103
|
+
# the user opted in to precompiled modules but React Native is still set
|
|
104
|
+
# to build from source, fall back to source-built modules and warn once.
|
|
85
105
|
def enabled?
|
|
86
|
-
ENV[ENV_VAR] == '1'
|
|
106
|
+
return false unless ENV[ENV_VAR] == '1'
|
|
107
|
+
return true if prebuilt_react_active?
|
|
108
|
+
|
|
109
|
+
unless @warned_no_prebuilt_react
|
|
110
|
+
@warned_no_prebuilt_react = true
|
|
111
|
+
Pod::UI.warn "[Expo] EXPO_USE_PRECOMPILED_MODULES=1 was set, but React Native is configured to build from source (RCT_USE_PREBUILT_RNCORE is not 1). Precompiled Expo modules require the prebuilt React.xcframework, so every Expo module will be built from source for this install. To use precompiled modules, ensure `ios.buildReactNativeFromSource` is not `true` in the `expo-build-properties` plugin (the default uses the prebuilt framework), or export RCT_USE_PREBUILT_RNCORE=1 before running `pod install`."
|
|
112
|
+
end
|
|
113
|
+
false
|
|
87
114
|
end
|
|
88
115
|
|
|
89
116
|
# Sets the list of package name patterns that should be built from source
|
|
@@ -104,6 +131,19 @@ module Expo
|
|
|
104
131
|
}
|
|
105
132
|
end
|
|
106
133
|
|
|
134
|
+
# @react-native-community/cli autolinking output (same shape as RN CLI's `config`).
|
|
135
|
+
# Used to locate 3rd-party packages via real node resolution so non-flat node_modules
|
|
136
|
+
# layouts (pnpm non-hoisted, yarn PnP, aliased specifiers) resolve correctly.
|
|
137
|
+
def react_native_config
|
|
138
|
+
@react_native_config ||= invoke_autolinking('react-native-config', platform: 'ios')
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# The resolved Expo modules list. Used by scan_node_modules_configs to locate
|
|
142
|
+
# each internal package's spm.config.json via its resolved podspec dir.
|
|
143
|
+
def resolved_modules
|
|
144
|
+
@resolved_modules ||= invoke_autolinking('resolve', platform: 'apple').fetch('modules', [])
|
|
145
|
+
end
|
|
146
|
+
|
|
107
147
|
# ──────────────────────────────────────────────────────────────────────
|
|
108
148
|
# Facade methods — called from installer.rb / autolinking_manager.rb
|
|
109
149
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -213,7 +253,7 @@ module Expo
|
|
|
213
253
|
def has_prebuilt_xcframework?(pod_name)
|
|
214
254
|
return false unless enabled?
|
|
215
255
|
|
|
216
|
-
|
|
256
|
+
resolve_prebuilt_status(pod_name)[:available]
|
|
217
257
|
end
|
|
218
258
|
|
|
219
259
|
# Returns whether test specs should be included for a pod.
|
|
@@ -456,13 +496,13 @@ module Expo
|
|
|
456
496
|
def try_link_with_prebuilt_xcframework(spec)
|
|
457
497
|
return false unless enabled?
|
|
458
498
|
|
|
459
|
-
|
|
460
|
-
unless
|
|
461
|
-
log_linking_status(spec.name, false,
|
|
499
|
+
resolution = resolve_prebuilt_status(spec.name)
|
|
500
|
+
unless resolution[:available]
|
|
501
|
+
log_linking_status(spec.name, false, resolution)
|
|
462
502
|
return false
|
|
463
503
|
end
|
|
464
504
|
|
|
465
|
-
pod_info, product_name, default_tarball = resolved
|
|
505
|
+
pod_info, product_name, default_tarball = resolution[:resolved]
|
|
466
506
|
|
|
467
507
|
log_linking_status(spec.name, true, default_tarball)
|
|
468
508
|
|
|
@@ -491,10 +531,13 @@ module Expo
|
|
|
491
531
|
# @param spec [Pod::Specification] The podspec to patch
|
|
492
532
|
# @return [Pod::Specification] A new patched specification (or original on failure)
|
|
493
533
|
def patch_spec_for_prebuilt(spec)
|
|
494
|
-
|
|
495
|
-
|
|
534
|
+
resolution = resolve_prebuilt_status(spec.name)
|
|
535
|
+
unless resolution[:available]
|
|
536
|
+
log_linking_status(spec.name, false, resolution) if resolution[:reason] == :dependency_unavailable
|
|
537
|
+
return spec
|
|
538
|
+
end
|
|
496
539
|
|
|
497
|
-
pod_info, product_name, default_tarball = resolved
|
|
540
|
+
pod_info, product_name, default_tarball = resolution[:resolved]
|
|
498
541
|
|
|
499
542
|
log_linking_status(spec.name, true, default_tarball)
|
|
500
543
|
|
|
@@ -563,13 +606,19 @@ module Expo
|
|
|
563
606
|
prefix = "[Expo-precompiled] ".blue
|
|
564
607
|
Pod::UI.info "#{prefix}Precompiled modules:"
|
|
565
608
|
@linked_pods.sort_by { |name, _| name.downcase }.each do |pod_name, info|
|
|
609
|
+
version = installed_version_for(pod_name)
|
|
610
|
+
version_suffix = version ? " #{"(#{version})".dark}" : ""
|
|
566
611
|
if info[:found]
|
|
567
|
-
Pod::UI.info "#{prefix} 📦 #{pod_name.green}"
|
|
612
|
+
Pod::UI.info "#{prefix} 📦 #{pod_name.green}#{version_suffix}"
|
|
568
613
|
else
|
|
569
|
-
|
|
614
|
+
reason = format_prebuilt_unavailable_reason(info)
|
|
615
|
+
Pod::UI.info "#{prefix} ⚠️ #{pod_name}#{version_suffix} #{"(#{reason})".dark}"
|
|
570
616
|
end
|
|
617
|
+
spm_versions = pod_lookup_map.dig(pod_name, :spm_dependency_versions) || {}
|
|
571
618
|
info[:spm_deps].each do |dep_name|
|
|
572
|
-
|
|
619
|
+
dep_version = spm_versions[dep_name]
|
|
620
|
+
dep_suffix = dep_version ? " #{"(#{dep_version})".dark}" : ""
|
|
621
|
+
Pod::UI.info "#{prefix} ∟ #{"#{dep_name}.xcframework".green}#{dep_suffix}"
|
|
573
622
|
end
|
|
574
623
|
end
|
|
575
624
|
|
|
@@ -578,6 +627,19 @@ module Expo
|
|
|
578
627
|
end
|
|
579
628
|
end
|
|
580
629
|
|
|
630
|
+
# Returns the installed version for a pod by reading its package.json, or nil.
|
|
631
|
+
def installed_version_for(pod_name)
|
|
632
|
+
package_root = pod_lookup_map.dig(pod_name, :package_root)
|
|
633
|
+
return nil unless package_root
|
|
634
|
+
|
|
635
|
+
pkg_json = File.join(package_root, 'package.json')
|
|
636
|
+
return nil unless File.exist?(pkg_json)
|
|
637
|
+
|
|
638
|
+
JSON.parse(File.read(pkg_json))['version']
|
|
639
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
640
|
+
nil
|
|
641
|
+
end
|
|
642
|
+
|
|
581
643
|
# ──────────────────────────────────────────────────────────────────────
|
|
582
644
|
# Post-install configuration steps
|
|
583
645
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -606,7 +668,7 @@ module Expo
|
|
|
606
668
|
|
|
607
669
|
# Copy both flavor tarballs
|
|
608
670
|
['debug', 'release'].each do |flavor|
|
|
609
|
-
src =
|
|
671
|
+
src = resolve_prebuilt_tarball(info, product_name, flavor, pod_name)
|
|
610
672
|
dst = File.join(artifacts_dir, "#{product_name}-#{flavor}.tar.gz")
|
|
611
673
|
FileUtils.cp(src, dst) if File.exist?(src) && !File.exist?(dst)
|
|
612
674
|
end
|
|
@@ -618,7 +680,7 @@ module Expo
|
|
|
618
680
|
# Self-healing: extract xcframework if missing (CocoaPods cache issue)
|
|
619
681
|
xcframework_dir = File.join(pod_dir, "#{product_name}.xcframework")
|
|
620
682
|
unless File.directory?(xcframework_dir)
|
|
621
|
-
tarball =
|
|
683
|
+
tarball = resolve_prebuilt_tarball(info, product_name, build_flavor, pod_name)
|
|
622
684
|
if File.exist?(tarball)
|
|
623
685
|
Pod::UI.info "#{'[Expo-precompiled] '.blue}Extracting #{product_name}.xcframework (cache miss)"
|
|
624
686
|
system("tar", "xzf", tarball, "-C", pod_dir)
|
|
@@ -1224,68 +1286,76 @@ module Expo
|
|
|
1224
1286
|
scan_external_configs(repo_root)
|
|
1225
1287
|
end
|
|
1226
1288
|
|
|
1227
|
-
#
|
|
1228
|
-
#
|
|
1289
|
+
# Locates spm.config.json for internal Expo modules in standalone projects
|
|
1290
|
+
# (no monorepo root). Uses resolved podspec dirs so non-flat layouts work.
|
|
1229
1291
|
def scan_node_modules_configs(project_root)
|
|
1230
|
-
|
|
1231
|
-
|
|
1292
|
+
resolved_modules.each do |mod|
|
|
1293
|
+
podspec_dir = (mod['pods'] || []).map { |pod| pod['podspecDir'] }.compact.first
|
|
1294
|
+
next unless podspec_dir
|
|
1232
1295
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
process_spm_config(config_path, :internal, project_root)
|
|
1236
|
-
end
|
|
1296
|
+
config_path = spm_config_path_for_podspec_dir(podspec_dir)
|
|
1297
|
+
next unless config_path
|
|
1237
1298
|
|
|
1238
|
-
# Internal Expo scoped packages: node_modules/@scope/*/spm.config.json
|
|
1239
|
-
Dir.glob(File.join(node_modules, '@*', '*', 'spm.config.json')).each do |config_path|
|
|
1240
1299
|
process_spm_config(config_path, :internal, project_root)
|
|
1241
1300
|
end
|
|
1242
1301
|
|
|
1243
|
-
# External 3rd-party packages: bundled in external-configs/ios/
|
|
1244
1302
|
scan_external_configs(project_root)
|
|
1245
1303
|
end
|
|
1246
1304
|
|
|
1305
|
+
# Resolves spm.config.json from an Expo autolinking podspecDir. Most modules
|
|
1306
|
+
# report `<package>/ios`, while packages with root podspecs report `<package>`.
|
|
1307
|
+
def spm_config_path_for_podspec_dir(podspec_dir)
|
|
1308
|
+
[podspec_dir, File.dirname(podspec_dir)].uniq.each do |package_dir|
|
|
1309
|
+
config_path = File.join(package_dir, 'spm.config.json')
|
|
1310
|
+
return config_path if File.exist?(config_path)
|
|
1311
|
+
end
|
|
1312
|
+
|
|
1313
|
+
nil
|
|
1314
|
+
end
|
|
1315
|
+
|
|
1247
1316
|
# Scans spm.config.json files from external-configs/ios/ for 3rd-party packages
|
|
1248
1317
|
# (e.g. react-native-screens, react-native-svg) that don't ship their own spm.config.json.
|
|
1249
1318
|
# Shared by both monorepo and standalone project paths.
|
|
1250
1319
|
#
|
|
1251
|
-
# @param effective_root [String] The project or repo root used to
|
|
1320
|
+
# @param effective_root [String] The project or repo root (used to compute the default build output dir)
|
|
1252
1321
|
def scan_external_configs(effective_root)
|
|
1253
1322
|
external_configs_dir = File.join(__dir__, '..', '..', 'external-configs', 'ios')
|
|
1254
1323
|
return unless File.directory?(external_configs_dir)
|
|
1255
1324
|
|
|
1256
|
-
node_modules = File.join(effective_root, 'node_modules')
|
|
1257
|
-
|
|
1258
1325
|
# Non-scoped: external-configs/ios/*/spm.config.json
|
|
1259
1326
|
Dir.glob(File.join(external_configs_dir, '*', 'spm.config.json')).each do |config_path|
|
|
1260
1327
|
npm_package = File.basename(File.dirname(config_path))
|
|
1261
|
-
process_external_config(config_path, npm_package,
|
|
1328
|
+
process_external_config(config_path, npm_package, effective_root)
|
|
1262
1329
|
end
|
|
1263
1330
|
|
|
1264
1331
|
# Scoped: external-configs/ios/@scope/*/spm.config.json
|
|
1265
1332
|
Dir.glob(File.join(external_configs_dir, '@*', '*', 'spm.config.json')).each do |config_path|
|
|
1266
1333
|
rel = config_path.sub("#{external_configs_dir}/", '')
|
|
1267
1334
|
npm_package = File.dirname(rel) # e.g. "@shopify/react-native-skia"
|
|
1268
|
-
process_external_config(config_path, npm_package,
|
|
1335
|
+
process_external_config(config_path, npm_package, effective_root)
|
|
1269
1336
|
end
|
|
1270
1337
|
end
|
|
1271
1338
|
|
|
1272
1339
|
# Processes a single external spm.config.json for a 3rd-party package.
|
|
1273
|
-
def process_external_config(config_path, npm_package,
|
|
1340
|
+
def process_external_config(config_path, npm_package, effective_root)
|
|
1274
1341
|
config = JSON.parse(File.read(config_path))
|
|
1275
1342
|
products = config['products'] || []
|
|
1276
|
-
package_root = File.join(node_modules, npm_package)
|
|
1277
1343
|
|
|
1278
|
-
#
|
|
1279
|
-
|
|
1344
|
+
# Resolve via rncli autolinking so we use real node resolution (handles pnpm,
|
|
1345
|
+
# yarn resolutions/PnP, aliased specifiers). Skip if the package isn't installed.
|
|
1346
|
+
dep = react_native_config.dig('dependencies', npm_package)
|
|
1347
|
+
return unless dep
|
|
1348
|
+
|
|
1349
|
+
package_root = dep['root']
|
|
1350
|
+
pkg_version = dep.dig('platforms', 'ios', 'version')
|
|
1280
1351
|
|
|
1281
|
-
#
|
|
1352
|
+
# codegenConfig.name isn't surfaced by rncli; read it from package.json.
|
|
1282
1353
|
installed_codegen_name = nil
|
|
1283
|
-
pkg_version = nil
|
|
1284
1354
|
pkg_json_path = File.join(package_root, 'package.json')
|
|
1285
1355
|
if File.exist?(pkg_json_path)
|
|
1286
1356
|
pkg_json = JSON.parse(File.read(pkg_json_path))
|
|
1287
1357
|
installed_codegen_name = pkg_json.dig('codegenConfig', 'name')
|
|
1288
|
-
pkg_version
|
|
1358
|
+
pkg_version ||= pkg_json['version']
|
|
1289
1359
|
end
|
|
1290
1360
|
|
|
1291
1361
|
base_dir = custom_modules_path || File.join(effective_root, 'packages', 'precompile', PRECOMPILE_BUILD_DIR)
|
|
@@ -1326,6 +1396,7 @@ module Expo
|
|
|
1326
1396
|
.map { |t| { name: t['name'], path: t['path'] } }
|
|
1327
1397
|
|
|
1328
1398
|
spm_dependency_frameworks = (product['spmPackages'] || []).map { |pkg| pkg['productName'] }.compact
|
|
1399
|
+
spm_dependency_versions = spm_package_versions(product['spmPackages'])
|
|
1329
1400
|
|
|
1330
1401
|
@pod_lookup_map[pod_name] = {
|
|
1331
1402
|
type: :external,
|
|
@@ -1337,6 +1408,8 @@ module Expo
|
|
|
1337
1408
|
product_name: product_name,
|
|
1338
1409
|
targets: targets,
|
|
1339
1410
|
spm_dependency_frameworks: spm_dependency_frameworks,
|
|
1411
|
+
spm_dependency_versions: spm_dependency_versions,
|
|
1412
|
+
prebuilt_dependency_pods: prebuilt_dependency_pods(product['externalDependencies']),
|
|
1340
1413
|
autolink_when: product['autolinkWhen']
|
|
1341
1414
|
}
|
|
1342
1415
|
end
|
|
@@ -1344,6 +1417,14 @@ module Expo
|
|
|
1344
1417
|
Pod::UI.warn "[Expo-precompiled] Failed to read external config at #{config_path}: #{e.message}"
|
|
1345
1418
|
end
|
|
1346
1419
|
|
|
1420
|
+
# Returns { productName => versionString } from a spm.config.json spmPackages array.
|
|
1421
|
+
def spm_package_versions(spm_packages)
|
|
1422
|
+
(spm_packages || []).each_with_object({}) do |pkg, h|
|
|
1423
|
+
version = pkg['version'].is_a?(Hash) ? pkg['version']['exact'] : pkg['version']
|
|
1424
|
+
h[pkg['productName']] = version if pkg['productName'] && version
|
|
1425
|
+
end
|
|
1426
|
+
end
|
|
1427
|
+
|
|
1347
1428
|
# Processes a single spm.config.json file and adds entries to @pod_lookup_map.
|
|
1348
1429
|
def process_spm_config(config_path, type, repo_root)
|
|
1349
1430
|
config = JSON.parse(File.read(config_path))
|
|
@@ -1397,6 +1478,7 @@ module Expo
|
|
|
1397
1478
|
.map { |t| { name: t['name'], path: t['path'] } }
|
|
1398
1479
|
|
|
1399
1480
|
spm_dependency_frameworks = (product['spmPackages'] || []).map { |pkg| pkg['productName'] }.compact
|
|
1481
|
+
spm_dependency_versions = spm_package_versions(product['spmPackages'])
|
|
1400
1482
|
|
|
1401
1483
|
{
|
|
1402
1484
|
type: type,
|
|
@@ -1408,10 +1490,31 @@ module Expo
|
|
|
1408
1490
|
product_name: product_name,
|
|
1409
1491
|
targets: targets,
|
|
1410
1492
|
spm_dependency_frameworks: spm_dependency_frameworks,
|
|
1493
|
+
spm_dependency_versions: spm_dependency_versions,
|
|
1494
|
+
prebuilt_dependency_pods: prebuilt_dependency_pods(product['externalDependencies']),
|
|
1411
1495
|
autolink_when: product['autolinkWhen']
|
|
1412
1496
|
}
|
|
1413
1497
|
end
|
|
1414
1498
|
|
|
1499
|
+
# Returns local Expo product dependencies whose prebuilt availability must
|
|
1500
|
+
# match this product's availability. Runtime deps like React/Hermes are not
|
|
1501
|
+
# encoded as package/product strings and are ignored here.
|
|
1502
|
+
def prebuilt_dependency_pods(external_dependencies)
|
|
1503
|
+
(external_dependencies || []).filter_map do |dep|
|
|
1504
|
+
next unless dep.is_a?(String) && dep.include?('/')
|
|
1505
|
+
|
|
1506
|
+
parts = dep.split('/')
|
|
1507
|
+
is_scoped = parts[0].start_with?('@')
|
|
1508
|
+
package_name = is_scoped ? "#{parts[0]}/#{parts[1]}" : parts[0]
|
|
1509
|
+
product_name = is_scoped ? parts[2] : parts[1]
|
|
1510
|
+
|
|
1511
|
+
next unless package_name&.start_with?('expo-', '@expo/')
|
|
1512
|
+
next if CUSTOM_XCFRAMEWORK_DEPENDENCIES.include?(product_name)
|
|
1513
|
+
|
|
1514
|
+
product_name
|
|
1515
|
+
end.uniq
|
|
1516
|
+
end
|
|
1517
|
+
|
|
1415
1518
|
# Resolves the codegen module name. For external packages, prefers codegenConfig.name
|
|
1416
1519
|
# from the installed package.json over spm.config.json's codegenName.
|
|
1417
1520
|
def resolve_codegen_name(product, pod_name, npm_package, type, repo_root)
|
|
@@ -1480,7 +1583,7 @@ module Expo
|
|
|
1480
1583
|
current_dir = start_dir || Dir.pwd
|
|
1481
1584
|
|
|
1482
1585
|
loop do
|
|
1483
|
-
return current_dir if File.
|
|
1586
|
+
return current_dir if File.exist?(File.join(current_dir, 'packages', 'expo-modules-core', 'spm.config.json'))
|
|
1484
1587
|
|
|
1485
1588
|
parent = File.dirname(current_dir)
|
|
1486
1589
|
break if parent == current_dir
|
|
@@ -1490,6 +1593,16 @@ module Expo
|
|
|
1490
1593
|
nil
|
|
1491
1594
|
end
|
|
1492
1595
|
|
|
1596
|
+
# Invokes `expo-modules-autolinking <subcommand> --json` and parses the output.
|
|
1597
|
+
# Used by `react_native_config` and `resolved_modules` above.
|
|
1598
|
+
def invoke_autolinking(subcommand, platform:)
|
|
1599
|
+
args = ['node', '--no-warnings', '--eval', "require('expo/bin/autolinking')",
|
|
1600
|
+
'expo-modules-autolinking', subcommand, '--platform', platform, '--json']
|
|
1601
|
+
JSON.parse(IO.popen(args, &:read))
|
|
1602
|
+
rescue => error
|
|
1603
|
+
raise "Failed to invoke `expo-modules-autolinking #{subcommand}`: #{error}"
|
|
1604
|
+
end
|
|
1605
|
+
|
|
1493
1606
|
# ──────────────────────────────────────────────────────────────────────
|
|
1494
1607
|
# Helpers: bundled frameworks
|
|
1495
1608
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1525,7 +1638,13 @@ module Expo
|
|
|
1525
1638
|
results = []
|
|
1526
1639
|
pod_lookup_map.each do |pod_name, info|
|
|
1527
1640
|
next unless info[:type] == :external
|
|
1528
|
-
|
|
1641
|
+
|
|
1642
|
+
unless has_prebuilt_xcframework?(pod_name)
|
|
1643
|
+
product_name = info[:product_name] || pod_name
|
|
1644
|
+
expected = File.join(info[:build_output_dir], build_flavor, 'xcframeworks', "#{product_name}.tar.gz")
|
|
1645
|
+
Pod::UI.warn "[Expo-precompiled] #{pod_name}: prebuilt xcframework not found. Expected tarball at #{expected}"
|
|
1646
|
+
next
|
|
1647
|
+
end
|
|
1529
1648
|
|
|
1530
1649
|
podspec_file = File.join(info[:podspec_dir], "#{pod_name}.podspec")
|
|
1531
1650
|
next unless File.exist?(podspec_file)
|
|
@@ -1565,11 +1684,15 @@ module Expo
|
|
|
1565
1684
|
# Helpers: version resolution for 3rd-party prebuild versioning
|
|
1566
1685
|
# ──────────────────────────────────────────────────────────────────────
|
|
1567
1686
|
|
|
1568
|
-
#
|
|
1687
|
+
# RN package path, as resolved by rncli (handles pnpm workspaces correctly).
|
|
1688
|
+
def react_native_path
|
|
1689
|
+
react_native_config['reactNativePath']
|
|
1690
|
+
end
|
|
1691
|
+
|
|
1569
1692
|
def react_native_version
|
|
1570
1693
|
@react_native_version ||= begin
|
|
1571
|
-
rn_pkg = File.join(
|
|
1572
|
-
File.exist?(rn_pkg) ? JSON.parse(File.read(rn_pkg))['version'] : nil
|
|
1694
|
+
rn_pkg = react_native_path && File.join(react_native_path, 'package.json')
|
|
1695
|
+
rn_pkg && File.exist?(rn_pkg) ? JSON.parse(File.read(rn_pkg))['version'] : nil
|
|
1573
1696
|
end
|
|
1574
1697
|
end
|
|
1575
1698
|
|
|
@@ -1577,37 +1700,21 @@ module Expo
|
|
|
1577
1700
|
# Mirrors the TypeScript resolution logic in tools/src/prebuilds/Utils.ts.
|
|
1578
1701
|
def hermes_version
|
|
1579
1702
|
@hermes_version ||= begin
|
|
1580
|
-
rn_path =
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
version
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
unless version
|
|
1593
|
-
tag_file = is_v1 ? '.hermesv1version' : '.hermesversion'
|
|
1594
|
-
tag_path = File.join(rn_path, 'sdks', tag_file)
|
|
1595
|
-
version = File.read(tag_path).strip if File.exist?(tag_path)
|
|
1596
|
-
end
|
|
1597
|
-
|
|
1598
|
-
# Normalize: strip "hermes-" prefix and "v" prefix
|
|
1599
|
-
version&.gsub(/^hermes-?/i, '')&.gsub(/^v/i, '')&.strip
|
|
1600
|
-
end
|
|
1601
|
-
end
|
|
1703
|
+
rn_path = react_native_path
|
|
1704
|
+
if rn_path
|
|
1705
|
+
is_v1 = ENV['RCT_HERMES_V1_ENABLED'] == '1'
|
|
1706
|
+
props_path = File.join(rn_path, 'sdks', 'hermes-engine', 'version.properties')
|
|
1707
|
+
version = File.exist?(props_path) ?
|
|
1708
|
+
parse_version_properties(props_path)[is_v1 ? 'HERMES_V1_VERSION_NAME' : 'HERMES_VERSION_NAME'] : nil
|
|
1709
|
+
|
|
1710
|
+
# Fallback to tag files
|
|
1711
|
+
unless version
|
|
1712
|
+
tag_path = File.join(rn_path, 'sdks', is_v1 ? '.hermesv1version' : '.hermesversion')
|
|
1713
|
+
version = File.read(tag_path).strip if File.exist?(tag_path)
|
|
1714
|
+
end
|
|
1602
1715
|
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
@node_modules_dir ||= begin
|
|
1606
|
-
repo_root = find_repo_root
|
|
1607
|
-
if repo_root
|
|
1608
|
-
File.join(repo_root, 'node_modules')
|
|
1609
|
-
else
|
|
1610
|
-
File.join(File.dirname(Dir.pwd), 'node_modules')
|
|
1716
|
+
# Normalize: strip "hermes-" prefix and "v" prefix
|
|
1717
|
+
version&.gsub(/^hermes-?/i, '')&.gsub(/^v/i, '')&.strip
|
|
1611
1718
|
end
|
|
1612
1719
|
end
|
|
1613
1720
|
end
|
|
@@ -1641,16 +1748,127 @@ module Expo
|
|
|
1641
1748
|
# Resolves prebuilt xcframework info for a pod.
|
|
1642
1749
|
# @return [Array, nil] [pod_info, product_name, tarball_path] or nil
|
|
1643
1750
|
def resolve_prebuilt_info(pod_name)
|
|
1644
|
-
|
|
1751
|
+
resolution = resolve_prebuilt_status(pod_name)
|
|
1752
|
+
resolution[:available] ? resolution[:resolved] : nil
|
|
1753
|
+
end
|
|
1645
1754
|
|
|
1755
|
+
# Resolves only this pod's own prebuilt artifact without checking parent dependencies.
|
|
1756
|
+
# @return [Hash] Availability information with :available, :resolved, :reason, and :path.
|
|
1757
|
+
def resolve_own_prebuilt_info(pod_name)
|
|
1646
1758
|
pod_info = pod_lookup_map[pod_name]
|
|
1647
|
-
return
|
|
1759
|
+
return { available: false, reason: :missing_config } unless pod_info
|
|
1648
1760
|
|
|
1649
1761
|
product_name = pod_info[:product_name] || pod_name
|
|
1650
|
-
tarball =
|
|
1651
|
-
return
|
|
1762
|
+
tarball = resolve_prebuilt_tarball(pod_info, product_name, build_flavor, pod_name)
|
|
1763
|
+
return { available: false, reason: :missing_tarball, path: tarball } unless File.exist?(tarball)
|
|
1652
1764
|
|
|
1653
|
-
[pod_info, product_name, tarball]
|
|
1765
|
+
{ available: true, resolved: [pod_info, product_name, tarball] }
|
|
1766
|
+
end
|
|
1767
|
+
|
|
1768
|
+
# A pod may use a prebuilt xcframework only when its own prebuilt artifact
|
|
1769
|
+
# exists and every local Expo dependency also uses prebuilt.
|
|
1770
|
+
def resolve_prebuilt_status(pod_name, visiting = Set.new)
|
|
1771
|
+
return { available: false, reason: :build_from_source } if build_from_source?(pod_name)
|
|
1772
|
+
return { available: true } if visiting.include?(pod_name)
|
|
1773
|
+
|
|
1774
|
+
own_resolution = resolve_own_prebuilt_info(pod_name)
|
|
1775
|
+
return own_resolution unless own_resolution[:available]
|
|
1776
|
+
|
|
1777
|
+
pod_info = own_resolution[:resolved][0]
|
|
1778
|
+
next_visiting = visiting.dup.add(pod_name)
|
|
1779
|
+
|
|
1780
|
+
(pod_info[:prebuilt_dependency_pods] || []).each do |dep_name|
|
|
1781
|
+
dep_resolution = resolve_prebuilt_status(dep_name, next_visiting)
|
|
1782
|
+
next if dep_resolution[:available]
|
|
1783
|
+
|
|
1784
|
+
return {
|
|
1785
|
+
available: false,
|
|
1786
|
+
reason: :dependency_unavailable,
|
|
1787
|
+
dependency: dep_name,
|
|
1788
|
+
dependency_reason: dep_resolution[:reason],
|
|
1789
|
+
dependency_path: dep_resolution[:path]
|
|
1790
|
+
}
|
|
1791
|
+
end
|
|
1792
|
+
|
|
1793
|
+
own_resolution
|
|
1794
|
+
end
|
|
1795
|
+
|
|
1796
|
+
def resolve_prebuilt_tarball(pod_info, product_name, flavor, pod_name = nil)
|
|
1797
|
+
tarball = File.join(pod_info[:build_output_dir], flavor, 'xcframeworks', "#{product_name}.tar.gz")
|
|
1798
|
+
return tarball if File.exist?(tarball)
|
|
1799
|
+
|
|
1800
|
+
base_url = external_modules_base_url
|
|
1801
|
+
return tarball unless pod_info[:type] == :external && base_url
|
|
1802
|
+
|
|
1803
|
+
output_prefix = File.join(pod_info[:npm_package], 'output')
|
|
1804
|
+
idx = pod_info[:build_output_dir].rindex(output_prefix)
|
|
1805
|
+
relative_path = [
|
|
1806
|
+
(idx ? pod_info[:build_output_dir][idx..] : output_prefix),
|
|
1807
|
+
flavor,
|
|
1808
|
+
'xcframeworks',
|
|
1809
|
+
"#{product_name}.tar.gz"
|
|
1810
|
+
].join('/')
|
|
1811
|
+
remote_url = "#{base_url.chomp('/')}/#{relative_path}"
|
|
1812
|
+
remote_tarball = File.join(remote_precompiled_artifacts_dir, relative_path)
|
|
1813
|
+
|
|
1814
|
+
return remote_tarball if File.exist?(remote_tarball)
|
|
1815
|
+
return remote_tarball if failed_remote_downloads.include?(remote_url)
|
|
1816
|
+
|
|
1817
|
+
download_remote_tarball(remote_url, remote_tarball, pod_name || product_name, flavor)
|
|
1818
|
+
remote_tarball
|
|
1819
|
+
end
|
|
1820
|
+
|
|
1821
|
+
def failed_remote_downloads
|
|
1822
|
+
@failed_remote_downloads ||= Set.new
|
|
1823
|
+
end
|
|
1824
|
+
|
|
1825
|
+
def remote_precompiled_artifacts_dir
|
|
1826
|
+
pods_root = Pod::Config.instance.sandbox_root rescue File.join(Dir.pwd, 'Pods')
|
|
1827
|
+
File.join(pods_root.to_s, 'ExpoPrecompiledArtifacts')
|
|
1828
|
+
end
|
|
1829
|
+
|
|
1830
|
+
def gray(text)
|
|
1831
|
+
text.respond_to?(:gray) ? text.gray : text
|
|
1832
|
+
end
|
|
1833
|
+
|
|
1834
|
+
def download_remote_tarball(remote_url, destination_path, pod_name, flavor)
|
|
1835
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
|
1836
|
+
tmp_path = "#{destination_path}.download-#{Process.pid}"
|
|
1837
|
+
|
|
1838
|
+
download_to_file(remote_url, tmp_path)
|
|
1839
|
+
FileUtils.mv(tmp_path, destination_path)
|
|
1840
|
+
Pod::UI.info "#{'[Expo-precompiled] '.blue}#{pod_name}: downloaded remote #{flavor} artifact"
|
|
1841
|
+
destination_path
|
|
1842
|
+
rescue => e
|
|
1843
|
+
FileUtils.rm_f(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
1844
|
+
failed_remote_downloads.add(remote_url)
|
|
1845
|
+
Pod::UI.puts "#{'[Expo-precompiled] '.blue}#{"#{pod_name}: remote #{flavor} artifact unavailable (#{e.message}); building from source".yellow}"
|
|
1846
|
+
Pod::UI.puts "#{'[Expo-precompiled] '.blue}#{gray(" URL: #{remote_url}")}"
|
|
1847
|
+
nil
|
|
1848
|
+
end
|
|
1849
|
+
|
|
1850
|
+
def download_to_file(url, destination_path, limit = 5)
|
|
1851
|
+
raise 'Too many HTTP redirects' if limit <= 0
|
|
1852
|
+
|
|
1853
|
+
uri = URI.parse(url)
|
|
1854
|
+
|
|
1855
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.is_a?(URI::HTTPS), open_timeout: 10, read_timeout: 120) do |http|
|
|
1856
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
1857
|
+
|
|
1858
|
+
http.request(request) do |response|
|
|
1859
|
+
case response
|
|
1860
|
+
when Net::HTTPSuccess
|
|
1861
|
+
File.open(destination_path, 'wb') do |file|
|
|
1862
|
+
response.read_body { |chunk| file.write(chunk) }
|
|
1863
|
+
end
|
|
1864
|
+
when Net::HTTPRedirection
|
|
1865
|
+
redirect_url = URI.join(uri.to_s, response['location']).to_s
|
|
1866
|
+
return download_to_file(redirect_url, destination_path, limit - 1)
|
|
1867
|
+
else
|
|
1868
|
+
raise "HTTP #{response.code}"
|
|
1869
|
+
end
|
|
1870
|
+
end
|
|
1871
|
+
end
|
|
1654
1872
|
end
|
|
1655
1873
|
|
|
1656
1874
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1863,10 +2081,27 @@ module Expo
|
|
|
1863
2081
|
# ──────────────────────────────────────────────────────────────────────
|
|
1864
2082
|
|
|
1865
2083
|
# Records the linking status for a pod (only once per pod to avoid duplicates).
|
|
1866
|
-
def log_linking_status(pod_name, found,
|
|
2084
|
+
def log_linking_status(pod_name, found, info = nil)
|
|
1867
2085
|
@linked_pods ||= {}
|
|
1868
2086
|
return if @linked_pods[pod_name]
|
|
1869
|
-
|
|
2087
|
+
status = info.is_a?(Hash) ? info.dup : { path: info }
|
|
2088
|
+
@linked_pods[pod_name] = status.merge(found: found, spm_deps: [])
|
|
2089
|
+
end
|
|
2090
|
+
|
|
2091
|
+
def format_prebuilt_unavailable_reason(info)
|
|
2092
|
+
case info[:reason]
|
|
2093
|
+
when :build_from_source
|
|
2094
|
+
'configured by buildFromSource'
|
|
2095
|
+
when :missing_config
|
|
2096
|
+
'prebuilt config not found'
|
|
2097
|
+
when :missing_tarball
|
|
2098
|
+
'prebuilt tarball not found'
|
|
2099
|
+
when :dependency_unavailable
|
|
2100
|
+
reason = format_prebuilt_unavailable_reason(reason: info[:dependency_reason], path: info[:dependency_path])
|
|
2101
|
+
"dependency #{info[:dependency]} is not using prebuilt: #{reason}"
|
|
2102
|
+
else
|
|
2103
|
+
info[:path] || 'prebuilt unavailable'
|
|
2104
|
+
end
|
|
1870
2105
|
end
|
|
1871
2106
|
|
|
1872
2107
|
# Records an SPM dependency xcframework bundled inside a precompiled pod.
|