expo-modules-autolinking 55.0.18 → 55.0.19
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 +9 -0
- package/external-configs/ios/README.md +8 -0
- package/package.json +2 -2
- package/scripts/ios/precompiled_modules.rb +280 -77
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,15 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 55.0.19 — 2026-04-28
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [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))
|
|
18
|
+
- [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))
|
|
19
|
+
- [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))
|
|
20
|
+
- [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))
|
|
21
|
+
|
|
13
22
|
## 55.0.18 — 2026-04-21
|
|
14
23
|
|
|
15
24
|
### 🐛 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.19",
|
|
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": "be06cb45cb9eb8076b6910daa98813a6a3b03287"
|
|
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
|
|
@@ -81,6 +90,11 @@ module Expo
|
|
|
81
90
|
ENV[MODULES_PATH_ENV_VAR]
|
|
82
91
|
end
|
|
83
92
|
|
|
93
|
+
# Returns the shared base URL for remote external prebuilt artifacts, if set.
|
|
94
|
+
def external_modules_base_url
|
|
95
|
+
ENV[EXTERNAL_MODULES_BASE_URL_ENV_VAR]
|
|
96
|
+
end
|
|
97
|
+
|
|
84
98
|
# Returns true if precompiled modules are enabled via environment variable
|
|
85
99
|
def enabled?
|
|
86
100
|
ENV[ENV_VAR] == '1'
|
|
@@ -104,6 +118,19 @@ module Expo
|
|
|
104
118
|
}
|
|
105
119
|
end
|
|
106
120
|
|
|
121
|
+
# @react-native-community/cli autolinking output (same shape as RN CLI's `config`).
|
|
122
|
+
# Used to locate 3rd-party packages via real node resolution so non-flat node_modules
|
|
123
|
+
# layouts (pnpm non-hoisted, yarn PnP, aliased specifiers) resolve correctly.
|
|
124
|
+
def react_native_config
|
|
125
|
+
@react_native_config ||= invoke_autolinking('react-native-config', platform: 'ios')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The resolved Expo modules list. Used by scan_node_modules_configs to locate
|
|
129
|
+
# each internal package's spm.config.json via its resolved podspec dir.
|
|
130
|
+
def resolved_modules
|
|
131
|
+
@resolved_modules ||= invoke_autolinking('resolve', platform: 'apple').fetch('modules', [])
|
|
132
|
+
end
|
|
133
|
+
|
|
107
134
|
# ──────────────────────────────────────────────────────────────────────
|
|
108
135
|
# Facade methods — called from installer.rb / autolinking_manager.rb
|
|
109
136
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -213,7 +240,7 @@ module Expo
|
|
|
213
240
|
def has_prebuilt_xcframework?(pod_name)
|
|
214
241
|
return false unless enabled?
|
|
215
242
|
|
|
216
|
-
|
|
243
|
+
resolve_prebuilt_status(pod_name)[:available]
|
|
217
244
|
end
|
|
218
245
|
|
|
219
246
|
# Returns whether test specs should be included for a pod.
|
|
@@ -456,13 +483,13 @@ module Expo
|
|
|
456
483
|
def try_link_with_prebuilt_xcframework(spec)
|
|
457
484
|
return false unless enabled?
|
|
458
485
|
|
|
459
|
-
|
|
460
|
-
unless
|
|
461
|
-
log_linking_status(spec.name, false,
|
|
486
|
+
resolution = resolve_prebuilt_status(spec.name)
|
|
487
|
+
unless resolution[:available]
|
|
488
|
+
log_linking_status(spec.name, false, resolution)
|
|
462
489
|
return false
|
|
463
490
|
end
|
|
464
491
|
|
|
465
|
-
pod_info, product_name, default_tarball = resolved
|
|
492
|
+
pod_info, product_name, default_tarball = resolution[:resolved]
|
|
466
493
|
|
|
467
494
|
log_linking_status(spec.name, true, default_tarball)
|
|
468
495
|
|
|
@@ -491,10 +518,13 @@ module Expo
|
|
|
491
518
|
# @param spec [Pod::Specification] The podspec to patch
|
|
492
519
|
# @return [Pod::Specification] A new patched specification (or original on failure)
|
|
493
520
|
def patch_spec_for_prebuilt(spec)
|
|
494
|
-
|
|
495
|
-
|
|
521
|
+
resolution = resolve_prebuilt_status(spec.name)
|
|
522
|
+
unless resolution[:available]
|
|
523
|
+
log_linking_status(spec.name, false, resolution) if resolution[:reason] == :dependency_unavailable
|
|
524
|
+
return spec
|
|
525
|
+
end
|
|
496
526
|
|
|
497
|
-
pod_info, product_name, default_tarball = resolved
|
|
527
|
+
pod_info, product_name, default_tarball = resolution[:resolved]
|
|
498
528
|
|
|
499
529
|
log_linking_status(spec.name, true, default_tarball)
|
|
500
530
|
|
|
@@ -563,13 +593,19 @@ module Expo
|
|
|
563
593
|
prefix = "[Expo-precompiled] ".blue
|
|
564
594
|
Pod::UI.info "#{prefix}Precompiled modules:"
|
|
565
595
|
@linked_pods.sort_by { |name, _| name.downcase }.each do |pod_name, info|
|
|
596
|
+
version = installed_version_for(pod_name)
|
|
597
|
+
version_suffix = version ? " #{"(#{version})".dark}" : ""
|
|
566
598
|
if info[:found]
|
|
567
|
-
Pod::UI.info "#{prefix} 📦 #{pod_name.green}"
|
|
599
|
+
Pod::UI.info "#{prefix} 📦 #{pod_name.green}#{version_suffix}"
|
|
568
600
|
else
|
|
569
|
-
|
|
601
|
+
reason = format_prebuilt_unavailable_reason(info)
|
|
602
|
+
Pod::UI.info "#{prefix} ⚠️ #{pod_name}#{version_suffix} #{"(#{reason})".dark}"
|
|
570
603
|
end
|
|
604
|
+
spm_versions = pod_lookup_map.dig(pod_name, :spm_dependency_versions) || {}
|
|
571
605
|
info[:spm_deps].each do |dep_name|
|
|
572
|
-
|
|
606
|
+
dep_version = spm_versions[dep_name]
|
|
607
|
+
dep_suffix = dep_version ? " #{"(#{dep_version})".dark}" : ""
|
|
608
|
+
Pod::UI.info "#{prefix} ∟ #{"#{dep_name}.xcframework".green}#{dep_suffix}"
|
|
573
609
|
end
|
|
574
610
|
end
|
|
575
611
|
|
|
@@ -578,6 +614,19 @@ module Expo
|
|
|
578
614
|
end
|
|
579
615
|
end
|
|
580
616
|
|
|
617
|
+
# Returns the installed version for a pod by reading its package.json, or nil.
|
|
618
|
+
def installed_version_for(pod_name)
|
|
619
|
+
package_root = pod_lookup_map.dig(pod_name, :package_root)
|
|
620
|
+
return nil unless package_root
|
|
621
|
+
|
|
622
|
+
pkg_json = File.join(package_root, 'package.json')
|
|
623
|
+
return nil unless File.exist?(pkg_json)
|
|
624
|
+
|
|
625
|
+
JSON.parse(File.read(pkg_json))['version']
|
|
626
|
+
rescue JSON::ParserError, Errno::ENOENT
|
|
627
|
+
nil
|
|
628
|
+
end
|
|
629
|
+
|
|
581
630
|
# ──────────────────────────────────────────────────────────────────────
|
|
582
631
|
# Post-install configuration steps
|
|
583
632
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -606,7 +655,7 @@ module Expo
|
|
|
606
655
|
|
|
607
656
|
# Copy both flavor tarballs
|
|
608
657
|
['debug', 'release'].each do |flavor|
|
|
609
|
-
src =
|
|
658
|
+
src = resolve_prebuilt_tarball(info, product_name, flavor, pod_name)
|
|
610
659
|
dst = File.join(artifacts_dir, "#{product_name}-#{flavor}.tar.gz")
|
|
611
660
|
FileUtils.cp(src, dst) if File.exist?(src) && !File.exist?(dst)
|
|
612
661
|
end
|
|
@@ -618,7 +667,7 @@ module Expo
|
|
|
618
667
|
# Self-healing: extract xcframework if missing (CocoaPods cache issue)
|
|
619
668
|
xcframework_dir = File.join(pod_dir, "#{product_name}.xcframework")
|
|
620
669
|
unless File.directory?(xcframework_dir)
|
|
621
|
-
tarball =
|
|
670
|
+
tarball = resolve_prebuilt_tarball(info, product_name, build_flavor, pod_name)
|
|
622
671
|
if File.exist?(tarball)
|
|
623
672
|
Pod::UI.info "#{'[Expo-precompiled] '.blue}Extracting #{product_name}.xcframework (cache miss)"
|
|
624
673
|
system("tar", "xzf", tarball, "-C", pod_dir)
|
|
@@ -1224,68 +1273,76 @@ module Expo
|
|
|
1224
1273
|
scan_external_configs(repo_root)
|
|
1225
1274
|
end
|
|
1226
1275
|
|
|
1227
|
-
#
|
|
1228
|
-
#
|
|
1276
|
+
# Locates spm.config.json for internal Expo modules in standalone projects
|
|
1277
|
+
# (no monorepo root). Uses resolved podspec dirs so non-flat layouts work.
|
|
1229
1278
|
def scan_node_modules_configs(project_root)
|
|
1230
|
-
|
|
1231
|
-
|
|
1279
|
+
resolved_modules.each do |mod|
|
|
1280
|
+
podspec_dir = (mod['pods'] || []).map { |pod| pod['podspecDir'] }.compact.first
|
|
1281
|
+
next unless podspec_dir
|
|
1232
1282
|
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
process_spm_config(config_path, :internal, project_root)
|
|
1236
|
-
end
|
|
1283
|
+
config_path = spm_config_path_for_podspec_dir(podspec_dir)
|
|
1284
|
+
next unless config_path
|
|
1237
1285
|
|
|
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
1286
|
process_spm_config(config_path, :internal, project_root)
|
|
1241
1287
|
end
|
|
1242
1288
|
|
|
1243
|
-
# External 3rd-party packages: bundled in external-configs/ios/
|
|
1244
1289
|
scan_external_configs(project_root)
|
|
1245
1290
|
end
|
|
1246
1291
|
|
|
1292
|
+
# Resolves spm.config.json from an Expo autolinking podspecDir. Most modules
|
|
1293
|
+
# report `<package>/ios`, while packages with root podspecs report `<package>`.
|
|
1294
|
+
def spm_config_path_for_podspec_dir(podspec_dir)
|
|
1295
|
+
[podspec_dir, File.dirname(podspec_dir)].uniq.each do |package_dir|
|
|
1296
|
+
config_path = File.join(package_dir, 'spm.config.json')
|
|
1297
|
+
return config_path if File.exist?(config_path)
|
|
1298
|
+
end
|
|
1299
|
+
|
|
1300
|
+
nil
|
|
1301
|
+
end
|
|
1302
|
+
|
|
1247
1303
|
# Scans spm.config.json files from external-configs/ios/ for 3rd-party packages
|
|
1248
1304
|
# (e.g. react-native-screens, react-native-svg) that don't ship their own spm.config.json.
|
|
1249
1305
|
# Shared by both monorepo and standalone project paths.
|
|
1250
1306
|
#
|
|
1251
|
-
# @param effective_root [String] The project or repo root used to
|
|
1307
|
+
# @param effective_root [String] The project or repo root (used to compute the default build output dir)
|
|
1252
1308
|
def scan_external_configs(effective_root)
|
|
1253
1309
|
external_configs_dir = File.join(__dir__, '..', '..', 'external-configs', 'ios')
|
|
1254
1310
|
return unless File.directory?(external_configs_dir)
|
|
1255
1311
|
|
|
1256
|
-
node_modules = File.join(effective_root, 'node_modules')
|
|
1257
|
-
|
|
1258
1312
|
# Non-scoped: external-configs/ios/*/spm.config.json
|
|
1259
1313
|
Dir.glob(File.join(external_configs_dir, '*', 'spm.config.json')).each do |config_path|
|
|
1260
1314
|
npm_package = File.basename(File.dirname(config_path))
|
|
1261
|
-
process_external_config(config_path, npm_package,
|
|
1315
|
+
process_external_config(config_path, npm_package, effective_root)
|
|
1262
1316
|
end
|
|
1263
1317
|
|
|
1264
1318
|
# Scoped: external-configs/ios/@scope/*/spm.config.json
|
|
1265
1319
|
Dir.glob(File.join(external_configs_dir, '@*', '*', 'spm.config.json')).each do |config_path|
|
|
1266
1320
|
rel = config_path.sub("#{external_configs_dir}/", '')
|
|
1267
1321
|
npm_package = File.dirname(rel) # e.g. "@shopify/react-native-skia"
|
|
1268
|
-
process_external_config(config_path, npm_package,
|
|
1322
|
+
process_external_config(config_path, npm_package, effective_root)
|
|
1269
1323
|
end
|
|
1270
1324
|
end
|
|
1271
1325
|
|
|
1272
1326
|
# Processes a single external spm.config.json for a 3rd-party package.
|
|
1273
|
-
def process_external_config(config_path, npm_package,
|
|
1327
|
+
def process_external_config(config_path, npm_package, effective_root)
|
|
1274
1328
|
config = JSON.parse(File.read(config_path))
|
|
1275
1329
|
products = config['products'] || []
|
|
1276
|
-
package_root = File.join(node_modules, npm_package)
|
|
1277
1330
|
|
|
1278
|
-
#
|
|
1279
|
-
|
|
1331
|
+
# Resolve via rncli autolinking so we use real node resolution (handles pnpm,
|
|
1332
|
+
# yarn resolutions/PnP, aliased specifiers). Skip if the package isn't installed.
|
|
1333
|
+
dep = react_native_config.dig('dependencies', npm_package)
|
|
1334
|
+
return unless dep
|
|
1335
|
+
|
|
1336
|
+
package_root = dep['root']
|
|
1337
|
+
pkg_version = dep.dig('platforms', 'ios', 'version')
|
|
1280
1338
|
|
|
1281
|
-
#
|
|
1339
|
+
# codegenConfig.name isn't surfaced by rncli; read it from package.json.
|
|
1282
1340
|
installed_codegen_name = nil
|
|
1283
|
-
pkg_version = nil
|
|
1284
1341
|
pkg_json_path = File.join(package_root, 'package.json')
|
|
1285
1342
|
if File.exist?(pkg_json_path)
|
|
1286
1343
|
pkg_json = JSON.parse(File.read(pkg_json_path))
|
|
1287
1344
|
installed_codegen_name = pkg_json.dig('codegenConfig', 'name')
|
|
1288
|
-
pkg_version
|
|
1345
|
+
pkg_version ||= pkg_json['version']
|
|
1289
1346
|
end
|
|
1290
1347
|
|
|
1291
1348
|
base_dir = custom_modules_path || File.join(effective_root, 'packages', 'precompile', PRECOMPILE_BUILD_DIR)
|
|
@@ -1326,6 +1383,7 @@ module Expo
|
|
|
1326
1383
|
.map { |t| { name: t['name'], path: t['path'] } }
|
|
1327
1384
|
|
|
1328
1385
|
spm_dependency_frameworks = (product['spmPackages'] || []).map { |pkg| pkg['productName'] }.compact
|
|
1386
|
+
spm_dependency_versions = spm_package_versions(product['spmPackages'])
|
|
1329
1387
|
|
|
1330
1388
|
@pod_lookup_map[pod_name] = {
|
|
1331
1389
|
type: :external,
|
|
@@ -1337,6 +1395,8 @@ module Expo
|
|
|
1337
1395
|
product_name: product_name,
|
|
1338
1396
|
targets: targets,
|
|
1339
1397
|
spm_dependency_frameworks: spm_dependency_frameworks,
|
|
1398
|
+
spm_dependency_versions: spm_dependency_versions,
|
|
1399
|
+
prebuilt_dependency_pods: prebuilt_dependency_pods(product['externalDependencies']),
|
|
1340
1400
|
autolink_when: product['autolinkWhen']
|
|
1341
1401
|
}
|
|
1342
1402
|
end
|
|
@@ -1344,6 +1404,14 @@ module Expo
|
|
|
1344
1404
|
Pod::UI.warn "[Expo-precompiled] Failed to read external config at #{config_path}: #{e.message}"
|
|
1345
1405
|
end
|
|
1346
1406
|
|
|
1407
|
+
# Returns { productName => versionString } from a spm.config.json spmPackages array.
|
|
1408
|
+
def spm_package_versions(spm_packages)
|
|
1409
|
+
(spm_packages || []).each_with_object({}) do |pkg, h|
|
|
1410
|
+
version = pkg['version'].is_a?(Hash) ? pkg['version']['exact'] : pkg['version']
|
|
1411
|
+
h[pkg['productName']] = version if pkg['productName'] && version
|
|
1412
|
+
end
|
|
1413
|
+
end
|
|
1414
|
+
|
|
1347
1415
|
# Processes a single spm.config.json file and adds entries to @pod_lookup_map.
|
|
1348
1416
|
def process_spm_config(config_path, type, repo_root)
|
|
1349
1417
|
config = JSON.parse(File.read(config_path))
|
|
@@ -1397,6 +1465,7 @@ module Expo
|
|
|
1397
1465
|
.map { |t| { name: t['name'], path: t['path'] } }
|
|
1398
1466
|
|
|
1399
1467
|
spm_dependency_frameworks = (product['spmPackages'] || []).map { |pkg| pkg['productName'] }.compact
|
|
1468
|
+
spm_dependency_versions = spm_package_versions(product['spmPackages'])
|
|
1400
1469
|
|
|
1401
1470
|
{
|
|
1402
1471
|
type: type,
|
|
@@ -1408,10 +1477,31 @@ module Expo
|
|
|
1408
1477
|
product_name: product_name,
|
|
1409
1478
|
targets: targets,
|
|
1410
1479
|
spm_dependency_frameworks: spm_dependency_frameworks,
|
|
1480
|
+
spm_dependency_versions: spm_dependency_versions,
|
|
1481
|
+
prebuilt_dependency_pods: prebuilt_dependency_pods(product['externalDependencies']),
|
|
1411
1482
|
autolink_when: product['autolinkWhen']
|
|
1412
1483
|
}
|
|
1413
1484
|
end
|
|
1414
1485
|
|
|
1486
|
+
# Returns local Expo product dependencies whose prebuilt availability must
|
|
1487
|
+
# match this product's availability. Runtime deps like React/Hermes are not
|
|
1488
|
+
# encoded as package/product strings and are ignored here.
|
|
1489
|
+
def prebuilt_dependency_pods(external_dependencies)
|
|
1490
|
+
(external_dependencies || []).filter_map do |dep|
|
|
1491
|
+
next unless dep.is_a?(String) && dep.include?('/')
|
|
1492
|
+
|
|
1493
|
+
parts = dep.split('/')
|
|
1494
|
+
is_scoped = parts[0].start_with?('@')
|
|
1495
|
+
package_name = is_scoped ? "#{parts[0]}/#{parts[1]}" : parts[0]
|
|
1496
|
+
product_name = is_scoped ? parts[2] : parts[1]
|
|
1497
|
+
|
|
1498
|
+
next unless package_name&.start_with?('expo-', '@expo/')
|
|
1499
|
+
next if CUSTOM_XCFRAMEWORK_DEPENDENCIES.include?(product_name)
|
|
1500
|
+
|
|
1501
|
+
product_name
|
|
1502
|
+
end.uniq
|
|
1503
|
+
end
|
|
1504
|
+
|
|
1415
1505
|
# Resolves the codegen module name. For external packages, prefers codegenConfig.name
|
|
1416
1506
|
# from the installed package.json over spm.config.json's codegenName.
|
|
1417
1507
|
def resolve_codegen_name(product, pod_name, npm_package, type, repo_root)
|
|
@@ -1490,6 +1580,16 @@ module Expo
|
|
|
1490
1580
|
nil
|
|
1491
1581
|
end
|
|
1492
1582
|
|
|
1583
|
+
# Invokes `expo-modules-autolinking <subcommand> --json` and parses the output.
|
|
1584
|
+
# Used by `react_native_config` and `resolved_modules` above.
|
|
1585
|
+
def invoke_autolinking(subcommand, platform:)
|
|
1586
|
+
args = ['node', '--no-warnings', '--eval', "require('expo/bin/autolinking')",
|
|
1587
|
+
'expo-modules-autolinking', subcommand, '--platform', platform, '--json']
|
|
1588
|
+
JSON.parse(IO.popen(args, &:read))
|
|
1589
|
+
rescue => error
|
|
1590
|
+
raise "Failed to invoke `expo-modules-autolinking #{subcommand}`: #{error}"
|
|
1591
|
+
end
|
|
1592
|
+
|
|
1493
1593
|
# ──────────────────────────────────────────────────────────────────────
|
|
1494
1594
|
# Helpers: bundled frameworks
|
|
1495
1595
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1525,7 +1625,13 @@ module Expo
|
|
|
1525
1625
|
results = []
|
|
1526
1626
|
pod_lookup_map.each do |pod_name, info|
|
|
1527
1627
|
next unless info[:type] == :external
|
|
1528
|
-
|
|
1628
|
+
|
|
1629
|
+
unless has_prebuilt_xcframework?(pod_name)
|
|
1630
|
+
product_name = info[:product_name] || pod_name
|
|
1631
|
+
expected = File.join(info[:build_output_dir], build_flavor, 'xcframeworks', "#{product_name}.tar.gz")
|
|
1632
|
+
Pod::UI.warn "[Expo-precompiled] #{pod_name}: prebuilt xcframework not found. Expected tarball at #{expected}"
|
|
1633
|
+
next
|
|
1634
|
+
end
|
|
1529
1635
|
|
|
1530
1636
|
podspec_file = File.join(info[:podspec_dir], "#{pod_name}.podspec")
|
|
1531
1637
|
next unless File.exist?(podspec_file)
|
|
@@ -1565,11 +1671,15 @@ module Expo
|
|
|
1565
1671
|
# Helpers: version resolution for 3rd-party prebuild versioning
|
|
1566
1672
|
# ──────────────────────────────────────────────────────────────────────
|
|
1567
1673
|
|
|
1568
|
-
#
|
|
1674
|
+
# RN package path, as resolved by rncli (handles pnpm workspaces correctly).
|
|
1675
|
+
def react_native_path
|
|
1676
|
+
react_native_config['reactNativePath']
|
|
1677
|
+
end
|
|
1678
|
+
|
|
1569
1679
|
def react_native_version
|
|
1570
1680
|
@react_native_version ||= begin
|
|
1571
|
-
rn_pkg = File.join(
|
|
1572
|
-
File.exist?(rn_pkg) ? JSON.parse(File.read(rn_pkg))['version'] : nil
|
|
1681
|
+
rn_pkg = react_native_path && File.join(react_native_path, 'package.json')
|
|
1682
|
+
rn_pkg && File.exist?(rn_pkg) ? JSON.parse(File.read(rn_pkg))['version'] : nil
|
|
1573
1683
|
end
|
|
1574
1684
|
end
|
|
1575
1685
|
|
|
@@ -1577,37 +1687,21 @@ module Expo
|
|
|
1577
1687
|
# Mirrors the TypeScript resolution logic in tools/src/prebuilds/Utils.ts.
|
|
1578
1688
|
def hermes_version
|
|
1579
1689
|
@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
|
|
1690
|
+
rn_path = react_native_path
|
|
1691
|
+
if rn_path
|
|
1692
|
+
is_v1 = ENV['RCT_HERMES_V1_ENABLED'] == '1'
|
|
1693
|
+
props_path = File.join(rn_path, 'sdks', 'hermes-engine', 'version.properties')
|
|
1694
|
+
version = File.exist?(props_path) ?
|
|
1695
|
+
parse_version_properties(props_path)[is_v1 ? 'HERMES_V1_VERSION_NAME' : 'HERMES_VERSION_NAME'] : nil
|
|
1696
|
+
|
|
1697
|
+
# Fallback to tag files
|
|
1698
|
+
unless version
|
|
1699
|
+
tag_path = File.join(rn_path, 'sdks', is_v1 ? '.hermesv1version' : '.hermesversion')
|
|
1700
|
+
version = File.read(tag_path).strip if File.exist?(tag_path)
|
|
1701
|
+
end
|
|
1602
1702
|
|
|
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')
|
|
1703
|
+
# Normalize: strip "hermes-" prefix and "v" prefix
|
|
1704
|
+
version&.gsub(/^hermes-?/i, '')&.gsub(/^v/i, '')&.strip
|
|
1611
1705
|
end
|
|
1612
1706
|
end
|
|
1613
1707
|
end
|
|
@@ -1641,16 +1735,108 @@ module Expo
|
|
|
1641
1735
|
# Resolves prebuilt xcframework info for a pod.
|
|
1642
1736
|
# @return [Array, nil] [pod_info, product_name, tarball_path] or nil
|
|
1643
1737
|
def resolve_prebuilt_info(pod_name)
|
|
1644
|
-
|
|
1738
|
+
resolution = resolve_prebuilt_status(pod_name)
|
|
1739
|
+
resolution[:available] ? resolution[:resolved] : nil
|
|
1740
|
+
end
|
|
1645
1741
|
|
|
1742
|
+
# Resolves only this pod's own prebuilt artifact without checking parent dependencies.
|
|
1743
|
+
# @return [Hash] Availability information with :available, :resolved, :reason, and :path.
|
|
1744
|
+
def resolve_own_prebuilt_info(pod_name)
|
|
1646
1745
|
pod_info = pod_lookup_map[pod_name]
|
|
1647
|
-
return
|
|
1746
|
+
return { available: false, reason: :missing_config } unless pod_info
|
|
1648
1747
|
|
|
1649
1748
|
product_name = pod_info[:product_name] || pod_name
|
|
1650
|
-
tarball =
|
|
1651
|
-
return
|
|
1749
|
+
tarball = resolve_prebuilt_tarball(pod_info, product_name, build_flavor, pod_name)
|
|
1750
|
+
return { available: false, reason: :missing_tarball, path: tarball } unless File.exist?(tarball)
|
|
1652
1751
|
|
|
1653
|
-
[pod_info, product_name, tarball]
|
|
1752
|
+
{ available: true, resolved: [pod_info, product_name, tarball] }
|
|
1753
|
+
end
|
|
1754
|
+
|
|
1755
|
+
# A pod may use a prebuilt xcframework only when its own prebuilt artifact
|
|
1756
|
+
# exists and every local Expo dependency also uses prebuilt.
|
|
1757
|
+
def resolve_prebuilt_status(pod_name, visiting = Set.new)
|
|
1758
|
+
return { available: false, reason: :build_from_source } if build_from_source?(pod_name)
|
|
1759
|
+
return { available: true } if visiting.include?(pod_name)
|
|
1760
|
+
|
|
1761
|
+
own_resolution = resolve_own_prebuilt_info(pod_name)
|
|
1762
|
+
return own_resolution unless own_resolution[:available]
|
|
1763
|
+
|
|
1764
|
+
pod_info = own_resolution[:resolved][0]
|
|
1765
|
+
next_visiting = visiting.dup.add(pod_name)
|
|
1766
|
+
|
|
1767
|
+
(pod_info[:prebuilt_dependency_pods] || []).each do |dep_name|
|
|
1768
|
+
dep_resolution = resolve_prebuilt_status(dep_name, next_visiting)
|
|
1769
|
+
next if dep_resolution[:available]
|
|
1770
|
+
|
|
1771
|
+
return {
|
|
1772
|
+
available: false,
|
|
1773
|
+
reason: :dependency_unavailable,
|
|
1774
|
+
dependency: dep_name,
|
|
1775
|
+
dependency_reason: dep_resolution[:reason],
|
|
1776
|
+
dependency_path: dep_resolution[:path]
|
|
1777
|
+
}
|
|
1778
|
+
end
|
|
1779
|
+
|
|
1780
|
+
own_resolution
|
|
1781
|
+
end
|
|
1782
|
+
|
|
1783
|
+
def resolve_prebuilt_tarball(pod_info, product_name, flavor, pod_name = nil)
|
|
1784
|
+
tarball = File.join(pod_info[:build_output_dir], flavor, 'xcframeworks', "#{product_name}.tar.gz")
|
|
1785
|
+
return tarball if File.exist?(tarball)
|
|
1786
|
+
|
|
1787
|
+
base_url = external_modules_base_url
|
|
1788
|
+
return tarball unless pod_info[:type] == :external && base_url
|
|
1789
|
+
|
|
1790
|
+
output_prefix = File.join(pod_info[:npm_package], 'output')
|
|
1791
|
+
idx = pod_info[:build_output_dir].rindex(output_prefix)
|
|
1792
|
+
relative_path = [
|
|
1793
|
+
(idx ? pod_info[:build_output_dir][idx..] : output_prefix),
|
|
1794
|
+
flavor,
|
|
1795
|
+
'xcframeworks',
|
|
1796
|
+
"#{product_name}.tar.gz"
|
|
1797
|
+
].join('/')
|
|
1798
|
+
remote_url = "#{base_url.chomp('/')}/#{relative_path}"
|
|
1799
|
+
|
|
1800
|
+
download_remote_tarball(remote_url, tarball, pod_name || product_name, flavor)
|
|
1801
|
+
end
|
|
1802
|
+
|
|
1803
|
+
def download_remote_tarball(remote_url, destination_path, pod_name, flavor)
|
|
1804
|
+
FileUtils.mkdir_p(File.dirname(destination_path))
|
|
1805
|
+
tmp_path = "#{destination_path}.download-#{Process.pid}"
|
|
1806
|
+
|
|
1807
|
+
Pod::UI.info "#{'[Expo-precompiled] '.blue}#{pod_name}: downloading #{flavor} artifact from #{remote_url}"
|
|
1808
|
+
|
|
1809
|
+
download_to_file(remote_url, tmp_path)
|
|
1810
|
+
FileUtils.mv(tmp_path, destination_path)
|
|
1811
|
+
destination_path
|
|
1812
|
+
rescue => e
|
|
1813
|
+
FileUtils.rm_f(tmp_path) if tmp_path && File.exist?(tmp_path)
|
|
1814
|
+
Pod::UI.warn "[Expo-precompiled] #{pod_name}: failed to download #{flavor} artifact from #{remote_url}: #{e.message}"
|
|
1815
|
+
nil
|
|
1816
|
+
end
|
|
1817
|
+
|
|
1818
|
+
def download_to_file(url, destination_path, limit = 5)
|
|
1819
|
+
raise 'Too many HTTP redirects' if limit <= 0
|
|
1820
|
+
|
|
1821
|
+
uri = URI.parse(url)
|
|
1822
|
+
|
|
1823
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.is_a?(URI::HTTPS), open_timeout: 10, read_timeout: 120) do |http|
|
|
1824
|
+
request = Net::HTTP::Get.new(uri.request_uri)
|
|
1825
|
+
|
|
1826
|
+
http.request(request) do |response|
|
|
1827
|
+
case response
|
|
1828
|
+
when Net::HTTPSuccess
|
|
1829
|
+
File.open(destination_path, 'wb') do |file|
|
|
1830
|
+
response.read_body { |chunk| file.write(chunk) }
|
|
1831
|
+
end
|
|
1832
|
+
when Net::HTTPRedirection
|
|
1833
|
+
redirect_url = URI.join(uri.to_s, response['location']).to_s
|
|
1834
|
+
return download_to_file(redirect_url, destination_path, limit - 1)
|
|
1835
|
+
else
|
|
1836
|
+
raise "HTTP #{response.code}"
|
|
1837
|
+
end
|
|
1838
|
+
end
|
|
1839
|
+
end
|
|
1654
1840
|
end
|
|
1655
1841
|
|
|
1656
1842
|
# ──────────────────────────────────────────────────────────────────────
|
|
@@ -1863,10 +2049,27 @@ module Expo
|
|
|
1863
2049
|
# ──────────────────────────────────────────────────────────────────────
|
|
1864
2050
|
|
|
1865
2051
|
# Records the linking status for a pod (only once per pod to avoid duplicates).
|
|
1866
|
-
def log_linking_status(pod_name, found,
|
|
2052
|
+
def log_linking_status(pod_name, found, info = nil)
|
|
1867
2053
|
@linked_pods ||= {}
|
|
1868
2054
|
return if @linked_pods[pod_name]
|
|
1869
|
-
|
|
2055
|
+
status = info.is_a?(Hash) ? info.dup : { path: info }
|
|
2056
|
+
@linked_pods[pod_name] = status.merge(found: found, spm_deps: [])
|
|
2057
|
+
end
|
|
2058
|
+
|
|
2059
|
+
def format_prebuilt_unavailable_reason(info)
|
|
2060
|
+
case info[:reason]
|
|
2061
|
+
when :build_from_source
|
|
2062
|
+
'configured by buildFromSource'
|
|
2063
|
+
when :missing_config
|
|
2064
|
+
'prebuilt config not found'
|
|
2065
|
+
when :missing_tarball
|
|
2066
|
+
'prebuilt tarball not found'
|
|
2067
|
+
when :dependency_unavailable
|
|
2068
|
+
reason = format_prebuilt_unavailable_reason(reason: info[:dependency_reason], path: info[:dependency_path])
|
|
2069
|
+
"dependency #{info[:dependency]} is not using prebuilt: #{reason}"
|
|
2070
|
+
else
|
|
2071
|
+
info[:path] || 'prebuilt unavailable'
|
|
2072
|
+
end
|
|
1870
2073
|
end
|
|
1871
2074
|
|
|
1872
2075
|
# Records an SPM dependency xcframework bundled inside a precompiled pod.
|