expo-modules-autolinking 56.0.9 → 56.0.11

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 CHANGED
@@ -10,6 +10,18 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.11 — 2026-05-21
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ - [iOS] Update `@shopify/react-native-skia` precompile config for the 2.6.x source layout ([#46081](https://github.com/expo/expo/pull/46081) by [@chrfalch](https://github.com/chrfalch))
18
+
19
+ ## 56.0.10 — 2026-05-21
20
+
21
+ ### 🎉 New features
22
+
23
+ - [iOS] Include and consume shared SPM dependencies in the precompiled pod / npm publish pipeline. ([#46069](https://github.com/expo/expo/pull/46069) by [@chrfalch](https://github.com/chrfalch))
24
+
13
25
  ## 56.0.9 — 2026-05-20
14
26
 
15
27
  ### 🐛 Bug fixes
@@ -1,209 +1,199 @@
1
1
  {
2
- "$schema": "../../../../../../tools/src/prebuilds/schemas/spm.config.schema.json",
3
- "products": [
2
+ "$schema": "../../../../../../tools/src/prebuilds/schemas/spm.config.schema.json",
3
+ "products": [
4
+ {
5
+ "name": "RNSkia",
6
+ "podName": "react-native-skia",
7
+ "codegenName": "rnskia",
8
+ "platforms": ["iOS(.v16)"],
9
+ "externalDependencies": ["ReactNativeDependencies", "React", "Hermes"],
10
+ "targets": [
4
11
  {
5
- "name": "RNSkia",
6
- "podName": "react-native-skia",
7
- "codegenName": "rnskia",
8
- "platforms": [
9
- "iOS(.v16)"
10
- ],
11
- "externalDependencies": [
12
- "ReactNativeDependencies",
13
- "React",
14
- "Hermes"
15
- ],
16
- "targets": [
17
- {
18
- "type": "framework",
19
- "name": "libskia",
20
- "path": "libs/apple/ios/libskia.xcframework"
21
- },
22
- {
23
- "type": "framework",
24
- "name": "libsvg",
25
- "path": "libs/apple/ios/libsvg.xcframework"
26
- },
27
- {
28
- "type": "framework",
29
- "name": "libskshaper",
30
- "path": "libs/apple/ios/libskshaper.xcframework"
31
- },
32
- {
33
- "type": "framework",
34
- "name": "libskparagraph",
35
- "path": "libs/apple/ios/libskparagraph.xcframework"
36
- },
37
- {
38
- "type": "framework",
39
- "name": "libskunicode_core",
40
- "path": "libs/apple/ios/libskunicode_core.xcframework"
41
- },
42
- {
43
- "type": "framework",
44
- "name": "libskunicode_libgrapheme",
45
- "path": "libs/apple/ios/libskunicode_libgrapheme.xcframework"
46
- },
47
- {
48
- "type": "framework",
49
- "name": "libskottie",
50
- "path": "libs/apple/ios/libskottie.xcframework"
51
- },
52
- {
53
- "type": "framework",
54
- "name": "libsksg",
55
- "path": "libs/apple/ios/libsksg.xcframework"
56
- },
57
- {
58
- "type": "objc",
59
- "name": "rnskia_codegen_modules",
60
- "moduleName": "rnskia",
61
- "path": ".build/codegen/build/generated/ios/ReactCodegen/rnskia",
62
- "pattern": "**/*.mm",
63
- "headerPattern": "**/*.h",
64
- "dependencies": [
65
- "React",
66
- "ReactNativeDependencies"
67
- ],
68
- "includeDirectories": [ ".." ]
69
- },
70
- {
71
- "type": "cpp",
72
- "name": "rnskia_codegen_components",
73
- "moduleName": "rnskia",
74
- "path": ".build/codegen/build/generated/ios/ReactCodegen/react/renderer/components/rnskia",
75
- "pattern": "**/*.cpp",
76
- "headerPattern": "**/*.h",
77
- "dependencies": [
78
- "React",
79
- "ReactNativeDependencies"
80
- ],
81
- "includeDirectories": [ "../../../.." ],
82
- "compilerFlags": {
83
- "common": {
84
- "cxx": [ "-fno-cxx-modules" ]
85
- }
86
- }
87
- },
88
- {
89
- "type": "cpp",
90
- "name": "RNSkia_cpp",
91
- "moduleName": "rnskia",
92
- "path": "cpp",
93
- "pattern": "**/*.cpp",
94
- "headerPattern": "**/*.h",
95
- "exclude": [
96
- "rnskia/RNDawnContext.h",
97
- "rnskia/RNDawnUtils.h",
98
- "rnskia/RNDawnWindowContext.h",
99
- "rnskia/RNDawnWindowContext.cpp",
100
- "rnskia/RNImageProvider.h",
101
- "rnwgpu/**",
102
- "skia/include/**",
103
- "skia/modules/skottie/**",
104
- "skia/modules/skparagraph/**",
105
- "skia/modules/sksg/**",
106
- "skia/modules/skshaper/**",
107
- "skia/modules/skunicode/**",
108
- "skia/modules/svg/**",
109
- "skia/src/**"
110
- ],
111
- "dependencies": [
112
- "React",
113
- "ReactNativeDependencies",
114
- "Hermes",
115
- "libskia",
116
- "libsvg",
117
- "libskshaper",
118
- "libskparagraph",
119
- "libskunicode_core",
120
- "libskunicode_libgrapheme",
121
- "libskottie",
122
- "libsksg"
123
- ],
124
- "includeDirectories": [
125
- ".",
126
- "api",
127
- "skia",
128
- "../.build/codegen/build/generated/ios/ReactCodegen"
129
- ],
130
- "compilerFlags": {
131
- "common": {
132
- "c": [
133
- "-DSK_METAL=1",
134
- "-DSK_GANESH=1",
135
- "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
136
- "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
137
- ],
138
- "cxx": [
139
- "-fno-cxx-modules",
140
- "-DSK_METAL=1",
141
- "-DSK_GANESH=1",
142
- "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
143
- "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
144
- ]
145
- },
146
- "debug": [ "-DHERMES_ENABLE_DEBUGGER=1" ]
147
- },
148
- "publicHeaders": false
149
- },
150
- {
151
- "type": "objc",
152
- "name": "RNSkia",
153
- "path": "apple",
154
- "pattern": "**/*.{m,mm}",
155
- "headerPattern": "**/*.h",
156
- "dependencies": [
157
- "Hermes",
158
- "React",
159
- "ReactNativeDependencies",
160
- "libskia",
161
- "libsvg",
162
- "libskshaper",
163
- "libskparagraph",
164
- "libskunicode_core",
165
- "libskunicode_libgrapheme",
166
- "libskottie",
167
- "libsksg",
168
- "rnskia_codegen_modules",
169
- "rnskia_codegen_components",
170
- "RNSkia_cpp"
171
- ],
172
- "includeDirectories": [
173
- ".",
174
- "../cpp",
175
- "../cpp/rnskia",
176
- "../cpp/jsi",
177
- "../cpp/api",
178
- "../cpp/utils",
179
- "../cpp/skia",
180
- "../.build/codegen/build/generated/ios/ReactCodegen"
181
- ],
182
- "linkedFrameworks": [
183
- "Foundation",
184
- "UIKit",
185
- "Metal",
186
- "MetalKit",
187
- "AVFoundation",
188
- "AVKit",
189
- "CoreMedia",
190
- "QuartzCore",
191
- "CoreGraphics",
192
- "CoreText",
193
- "CoreVideo",
194
- "CoreImage",
195
- "IOSurface"
196
- ],
197
- "compilerFlags": [
198
- "-include", "Foundation/Foundation.h",
199
- "-include", "UIKit/UIKit.h",
200
- "-DSK_METAL=1",
201
- "-DSK_GANESH=1",
202
- "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
203
- "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
204
- ]
205
- }
206
- ]
12
+ "type": "framework",
13
+ "name": "libskia",
14
+ "path": "libs/ios/libskia.xcframework"
15
+ },
16
+ {
17
+ "type": "framework",
18
+ "name": "libsvg",
19
+ "path": "libs/ios/libsvg.xcframework"
20
+ },
21
+ {
22
+ "type": "framework",
23
+ "name": "libskshaper",
24
+ "path": "libs/ios/libskshaper.xcframework"
25
+ },
26
+ {
27
+ "type": "framework",
28
+ "name": "libskparagraph",
29
+ "path": "libs/ios/libskparagraph.xcframework"
30
+ },
31
+ {
32
+ "type": "framework",
33
+ "name": "libskunicode_core",
34
+ "path": "libs/ios/libskunicode_core.xcframework"
35
+ },
36
+ {
37
+ "type": "framework",
38
+ "name": "libskunicode_libgrapheme",
39
+ "path": "libs/ios/libskunicode_libgrapheme.xcframework"
40
+ },
41
+ {
42
+ "type": "framework",
43
+ "name": "libskottie",
44
+ "path": "libs/ios/libskottie.xcframework"
45
+ },
46
+ {
47
+ "type": "framework",
48
+ "name": "libsksg",
49
+ "path": "libs/ios/libsksg.xcframework"
50
+ },
51
+ {
52
+ "type": "objc",
53
+ "name": "rnskia_codegen_modules",
54
+ "moduleName": "rnskia",
55
+ "path": ".build/codegen/build/generated/ios/ReactCodegen/rnskia",
56
+ "pattern": "**/*.mm",
57
+ "headerPattern": "**/*.h",
58
+ "dependencies": ["React", "ReactNativeDependencies"],
59
+ "includeDirectories": [".."]
60
+ },
61
+ {
62
+ "type": "cpp",
63
+ "name": "rnskia_codegen_components",
64
+ "moduleName": "rnskia",
65
+ "path": ".build/codegen/build/generated/ios/ReactCodegen/react/renderer/components/rnskia",
66
+ "pattern": "**/*.cpp",
67
+ "headerPattern": "**/*.h",
68
+ "dependencies": ["React", "ReactNativeDependencies"],
69
+ "includeDirectories": ["../../../.."],
70
+ "compilerFlags": {
71
+ "common": {
72
+ "cxx": ["-fno-cxx-modules"]
73
+ }
74
+ }
75
+ },
76
+ {
77
+ "type": "cpp",
78
+ "name": "RNSkia_cpp",
79
+ "moduleName": "rnskia",
80
+ "path": "cpp",
81
+ "pattern": "**/*.cpp",
82
+ "headerPattern": "**/*.h",
83
+ "exclude": [
84
+ "rnskia/RNDawnContext.h",
85
+ "rnskia/RNDawnUtils.h",
86
+ "rnskia/RNDawnWindowContext.h",
87
+ "rnskia/RNDawnWindowContext.cpp",
88
+ "rnskia/RNImageProvider.h",
89
+ "rnwgpu/**",
90
+ "skia/include/**",
91
+ "skia/modules/skottie/**",
92
+ "skia/modules/skparagraph/**",
93
+ "skia/modules/sksg/**",
94
+ "skia/modules/skshaper/**",
95
+ "skia/modules/skunicode/**",
96
+ "skia/modules/svg/**",
97
+ "skia/src/**"
98
+ ],
99
+ "dependencies": [
100
+ "React",
101
+ "ReactNativeDependencies",
102
+ "Hermes",
103
+ "libskia",
104
+ "libsvg",
105
+ "libskshaper",
106
+ "libskparagraph",
107
+ "libskunicode_core",
108
+ "libskunicode_libgrapheme",
109
+ "libskottie",
110
+ "libsksg"
111
+ ],
112
+ "includeDirectories": [
113
+ ".",
114
+ "api",
115
+ "skia",
116
+ "../.build/codegen/build/generated/ios/ReactCodegen"
117
+ ],
118
+ "compilerFlags": {
119
+ "common": {
120
+ "c": [
121
+ "-DSK_METAL=1",
122
+ "-DSK_GANESH=1",
123
+ "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
124
+ "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
125
+ ],
126
+ "cxx": [
127
+ "-fno-cxx-modules",
128
+ "-DSK_METAL=1",
129
+ "-DSK_GANESH=1",
130
+ "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
131
+ "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
132
+ ]
133
+ },
134
+ "debug": ["-DHERMES_ENABLE_DEBUGGER=1"]
135
+ },
136
+ "publicHeaders": false
137
+ },
138
+ {
139
+ "type": "objc",
140
+ "name": "RNSkia",
141
+ "path": "apple",
142
+ "pattern": "**/*.{m,mm}",
143
+ "headerPattern": "**/*.h",
144
+ "dependencies": [
145
+ "Hermes",
146
+ "React",
147
+ "ReactNativeDependencies",
148
+ "libskia",
149
+ "libsvg",
150
+ "libskshaper",
151
+ "libskparagraph",
152
+ "libskunicode_core",
153
+ "libskunicode_libgrapheme",
154
+ "libskottie",
155
+ "libsksg",
156
+ "rnskia_codegen_modules",
157
+ "rnskia_codegen_components",
158
+ "RNSkia_cpp"
159
+ ],
160
+ "includeDirectories": [
161
+ ".",
162
+ "../cpp",
163
+ "../cpp/rnskia",
164
+ "../cpp/jsi",
165
+ "../cpp/api",
166
+ "../cpp/utils",
167
+ "../cpp/skia",
168
+ "../.build/codegen/build/generated/ios/ReactCodegen"
169
+ ],
170
+ "linkedFrameworks": [
171
+ "Foundation",
172
+ "UIKit",
173
+ "Metal",
174
+ "MetalKit",
175
+ "AVFoundation",
176
+ "AVKit",
177
+ "CoreMedia",
178
+ "QuartzCore",
179
+ "CoreGraphics",
180
+ "CoreText",
181
+ "CoreVideo",
182
+ "CoreImage",
183
+ "IOSurface"
184
+ ],
185
+ "compilerFlags": [
186
+ "-include",
187
+ "Foundation/Foundation.h",
188
+ "-include",
189
+ "UIKit/UIKit.h",
190
+ "-DSK_METAL=1",
191
+ "-DSK_GANESH=1",
192
+ "-DSK_IMAGE_READ_PIXELS_DISABLE_LEGACY_API=1",
193
+ "-DSK_DISABLE_LEGACY_SHAPER_FACTORY=1"
194
+ ]
207
195
  }
208
- ]
196
+ ]
197
+ }
198
+ ]
209
199
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-autolinking",
3
- "version": "56.0.9",
3
+ "version": "56.0.11",
4
4
  "description": "Scripts that autolink Expo modules.",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",
@@ -49,7 +49,7 @@
49
49
  "chalk": "^4.1.0",
50
50
  "commander": "^7.2.0"
51
51
  },
52
- "gitHead": "c4c9867a0bcbb188e55ecaec4998e38d33108a5d",
52
+ "gitHead": "125e8225bf36a4b9b2a159441d9ea724bcf1110f",
53
53
  "scripts": {
54
54
  "build": "expo-module build",
55
55
  "clean": "expo-module clean",
@@ -54,6 +54,11 @@ module Expo
54
54
  # Centralized build output directory under packages/precompile/
55
55
  PRECOMPILE_BUILD_DIR = '.build'.freeze
56
56
 
57
+ # Subdir under packages/precompile/.build/ holding <Name>/<flavor>/<Name>.xcframework (monorepo source).
58
+ SHARED_SPM_DEPS_SOURCE_DIR = '.spm-deps'.freeze
59
+ # Subdir inside an npm package that bundles shared SPM xcframeworks for standalone consumers.
60
+ BUNDLED_SHARED_SPM_DEPS_SUBPATH = File.join('prebuilds', 'spm-deps').freeze
61
+
57
62
  # Apple platforms supported by CocoaPods podspecs
58
63
  APPLE_PLATFORMS = %w[ios osx tvos watchos visionos].freeze
59
64
 
@@ -188,10 +193,14 @@ module Expo
188
193
  # - Pods that vendor xcframeworks (already precompiled)
189
194
  # - Source-built pods that depend on React-Core (non-modular includes)
190
195
  #
196
+ # Also stages shared SPM dep xcframework symlinks inside their owner pod's
197
+ # directory — must run before `generate_pods_project` reads each xcframework's
198
+ # Info.plist to slice it.
199
+ #
191
200
  # @param installer [Pod::Installer] The CocoaPods installer instance
192
201
  def perform_pre_install(installer)
193
202
  return unless enabled?
194
- return unless prebuilt_react_active?
203
+ ensure_shared_spm_deps(installer)
195
204
  return if linkage(installer).nil?
196
205
 
197
206
  pods_to_downgrade = Set.new(installer.podfile.framework_modules_to_patch)
@@ -215,6 +224,63 @@ module Expo
215
224
  end
216
225
  end
217
226
 
227
+ # Symlinks each shared SPM dependency xcframework (e.g. SDWebImage) into the
228
+ # pod directory of its owner. Ownership is whatever `build_vendored_paths` set
229
+ # in `@framework_owner_map` during `store_podspec` (resolution-first);
230
+ # falls back to alphabetical-first only if the map has no entry. Must run
231
+ # before `generate_pods_project` so CocoaPods sees the symlinks when reading
232
+ # each xcframework's Info.plist.
233
+ def ensure_shared_spm_deps(installer)
234
+ return unless enabled?
235
+
236
+ consumers_by_dep = collect_shared_spm_deps(installer)
237
+ return if consumers_by_dep.empty?
238
+
239
+ @framework_owner_map ||= {}
240
+ @claimed_vendored_frameworks ||= Set.new
241
+
242
+ unresolved = []
243
+ staged = 0
244
+
245
+ consumers_by_dep.each do |dep_name, consumers|
246
+ existing = @framework_owner_map[dep_name]
247
+ owner_name = (existing && consumers.key?(existing)) ? existing : consumers.keys.sort.first
248
+ owner_info = consumers[owner_name]
249
+ @framework_owner_map[dep_name] ||= owner_name
250
+ @claimed_vendored_frameworks.add(dep_name)
251
+
252
+ source_path = shared_spm_dep_xcframework_path(dep_name, owner_info, build_flavor)
253
+ owner_pod_dir = File.join(installer.sandbox.root, owner_name)
254
+ unless source_path && File.directory?(owner_pod_dir)
255
+ unresolved << dep_name
256
+ next
257
+ end
258
+
259
+ FileUtils.rm_rf(File.join(owner_pod_dir, "#{dep_name}.xcframework"))
260
+ File.symlink(source_path, File.join(owner_pod_dir, "#{dep_name}.xcframework"))
261
+ staged += 1
262
+ end
263
+
264
+ if unresolved.any?
265
+ Pod::UI.warn "[Expo-precompiled] Shared SPM xcframeworks not found for: #{unresolved.join(', ')} (flavor: #{build_flavor}). The Expo modules that depend on them will fail at runtime with dyld 'Library not loaded: @rpath/<Name>.framework/<Name>'. Run the precompile prebuild pipeline, or ensure each consuming npm package ships prebuilds/spm-deps/<Name>/<flavor>/<Name>.xcframework."
266
+ end
267
+
268
+ Pod::UI.puts "[Expo] ".blue + "Staged #{staged}/#{consumers_by_dep.size} shared SPM xcframework(s) (#{build_flavor})" if staged > 0
269
+ end
270
+
271
+ # dep_name => { pod_name => pod_info } for shared SPM deps consumed by enabled prebuilt pods in this install.
272
+ def collect_shared_spm_deps(installer)
273
+ by_dep = {}
274
+ installer.pod_targets.each do |pod_target|
275
+ info = pod_lookup_map[pod_target.name]
276
+ next unless info && has_prebuilt_xcframework?(pod_target.name)
277
+ (info[:spm_dependency_frameworks] || []).each do |dep_name|
278
+ (by_dep[dep_name] ||= {})[pod_target.name] = info
279
+ end
280
+ end
281
+ by_dep
282
+ end
283
+
218
284
  # ──────────────────────────────────────────────────────────────────────
219
285
  # Cache management
220
286
  # ──────────────────────────────────────────────────────────────────────
@@ -1108,15 +1174,10 @@ module Expo
1108
1174
  end
1109
1175
  end
1110
1176
 
1111
- # Builds the vendored_frameworks paths array for a prebuilt pod.
1112
- # Deduplicates shared SPM dependency frameworks across multiple prebuilt pods:
1113
- # the first pod to claim a framework "owns" it; subsequent pods skip it and
1114
- # instead get FRAMEWORK_SEARCH_PATHS pointing at the owning pod's directory.
1115
- #
1116
- # @param product_name [String] The product/module name
1117
- # @param pod_info [Hash] Package info from spm.config.json lookup
1118
- # @param pod_name [String] The pod name (for summary tracking)
1119
- # @return [Array<String>] vendored framework paths
1177
+ # Returns vendored_frameworks paths for a prebuilt pod: the product's own
1178
+ # xcframework plus any shared SPM deps this pod owns (first to claim wins;
1179
+ # non-owners get FRAMEWORK_SEARCH_PATHS instead). Shared dep entries are
1180
+ # symlinks staged by `ensure_shared_spm_deps` inside the owner's pod dir.
1120
1181
  def build_vendored_paths(product_name, pod_info, pod_name)
1121
1182
  @claimed_vendored_frameworks ||= Set.new
1122
1183
  @framework_owner_map ||= {}
@@ -1126,38 +1187,27 @@ module Expo
1126
1187
  @framework_owner_map[product_name] = pod_name
1127
1188
 
1128
1189
  (pod_info[:spm_dependency_frameworks] || []).each do |dep_name|
1129
- if @claimed_vendored_frameworks.include?(dep_name)
1130
- owner = @framework_owner_map[dep_name]
1131
- Pod::UI.puts "#{'[Expo-precompiled] '.blue}Skipping #{dep_name}.xcframework from #{pod_name} — already vendored by #{owner}"
1132
- else
1190
+ owner = (@framework_owner_map[dep_name] ||= pod_name)
1191
+ if owner == pod_name
1133
1192
  paths << "#{dep_name}.xcframework"
1134
- @claimed_vendored_frameworks.add(dep_name)
1135
- @framework_owner_map[dep_name] = pod_name
1193
+ else
1194
+ Pod::UI.puts "#{'[Expo-precompiled] '.blue}Skipping #{dep_name}.xcframework from #{pod_name} — already vendored by #{owner}"
1136
1195
  end
1137
1196
  log_spm_dependency(pod_name, dep_name)
1138
1197
  end
1139
1198
  paths
1140
1199
  end
1141
1200
 
1142
- # Returns FRAMEWORK_SEARCH_PATHS entries for shared SPM dependency frameworks
1143
- # that were claimed by another prebuilt pod. The non-owning pod needs these
1144
- # paths so the linker can find the xcframeworks at build time.
1145
- #
1146
- # @param pod_name [String] The pod name
1147
- # @param pod_info [Hash] Package info from spm.config.json lookup
1148
- # @return [Array<String>] framework search path entries
1201
+ # FRAMEWORK_SEARCH_PATHS entries for shared SPM deps claimed by another pod.
1202
+ # CocoaPods slices each xcframework into `${PODS_XCFRAMEWORKS_BUILD_DIR}/<owner>/`,
1203
+ # which is where the linker resolves the framework.
1149
1204
  def framework_search_paths_for_skipped_deps(pod_name, pod_info)
1150
- @claimed_vendored_frameworks ||= Set.new
1151
1205
  @framework_owner_map ||= {}
1152
-
1153
- paths = []
1154
- (pod_info[:spm_dependency_frameworks] || []).each do |dep_name|
1206
+ owners = (pod_info[:spm_dependency_frameworks] || []).filter_map do |dep_name|
1155
1207
  owner = @framework_owner_map[dep_name]
1156
- if owner && owner != pod_name
1157
- paths << "\"${PODS_ROOT}/#{owner}\""
1158
- end
1159
- end
1160
- paths.uniq
1208
+ owner if owner && owner != pod_name
1209
+ end.uniq
1210
+ owners.flat_map { |owner| [%("${PODS_XCFRAMEWORKS_BUILD_DIR}/#{owner}"), %("${PODS_ROOT}/#{owner}")] }
1161
1211
  end
1162
1212
 
1163
1213
  # ──────────────────────────────────────────────────────────────────────
@@ -1184,11 +1234,13 @@ module Expo
1184
1234
  package_root_var = "#{pods_parent}/#{package_root_rel}"
1185
1235
  dsym_stamp = "$(DERIVED_FILE_DIR)/expo-dsym-resolve-#{product_name}-$(CONFIGURATION).stamp"
1186
1236
 
1237
+ shared_deps = shared_dep_switch_args(spec_name, pod_info)
1238
+
1187
1239
  switch_phase = {
1188
1240
  'name' => "[Expo] Switch #{spec_name} XCFramework for build configuration",
1189
1241
  'execution_position' => 'before_compile',
1190
1242
  'input_files' => ["#{pods_parent}/#{switch_script_rel}"],
1191
- 'script' => xcframework_switch_script(product_name, xcframeworks_dir_var, switch_script_path),
1243
+ 'script' => xcframework_switch_script(product_name, xcframeworks_dir_var, switch_script_path, shared_deps),
1192
1244
  }
1193
1245
 
1194
1246
  if Gem::Version.new(Pod::VERSION) >= Gem::Version.new('1.13.0')
@@ -1231,32 +1283,55 @@ module Expo
1231
1283
  SH
1232
1284
  end
1233
1285
 
1234
- # Returns the shell script for the xcframework switch phase.
1235
- def xcframework_switch_script(product_name, xcframeworks_dir, script_path)
1236
- <<~SH
1237
- # Switch between debug/release XCFramework based on build configuration
1238
- # This script is auto-generated by expo-modules-autolinking
1239
-
1286
+ # Shell script for the xcframework switch phase. With no shared deps the
1287
+ # script short-circuits in shell when the per-pod state file matches; with
1288
+ # shared deps Node is always invoked so each dep's symlink can be repointed
1289
+ # (it has its own per-dep state file inside replace-xcframework.js).
1290
+ def xcframework_switch_script(product_name, xcframeworks_dir, script_path, shared_deps = [])
1291
+ config_detect = <<~SH.chomp
1240
1292
  CONFIG="release"
1241
1293
  if echo "$GCC_PREPROCESSOR_DEFINITIONS" | grep -q "DEBUG=1"; then
1242
1294
  CONFIG="debug"
1243
1295
  fi
1244
-
1245
- # Early exit: Skip Node.js invocation if configuration hasn't changed
1246
- # This optimization avoids ~100-200ms overhead per module on incremental builds
1247
- LAST_CONFIG_FILE="#{xcframeworks_dir}/artifacts/.last_build_configuration"
1248
- if [ -f "$LAST_CONFIG_FILE" ] && [ "$(cat "$LAST_CONFIG_FILE")" = "$CONFIG" ]; then
1249
- exit 0
1250
- fi
1251
-
1252
- # Configuration changed or first build - invoke Node.js to extract tarball
1253
- . "$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
1254
-
1255
- "$NODE_BINARY" "#{script_path}" \\
1256
- -c "$CONFIG" \\
1257
- -m "#{product_name}" \\
1258
- -x "#{xcframeworks_dir}"
1259
1296
  SH
1297
+
1298
+ if shared_deps.empty?
1299
+ <<~SH
1300
+ # Auto-generated by expo-modules-autolinking
1301
+ #{config_detect}
1302
+ LAST_CONFIG_FILE="#{xcframeworks_dir}/artifacts/.last_build_configuration"
1303
+ if [ -f "$LAST_CONFIG_FILE" ] && [ "$(cat "$LAST_CONFIG_FILE")" = "$CONFIG" ]; then
1304
+ exit 0
1305
+ fi
1306
+ . "$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
1307
+ "$NODE_BINARY" "#{script_path}" -c "$CONFIG" -m "#{product_name}" -x "#{xcframeworks_dir}"
1308
+ SH
1309
+ else
1310
+ shared_args = shared_deps.map { |arg| " #{arg}" }.join(" \\\n")
1311
+ <<~SH
1312
+ # Auto-generated by expo-modules-autolinking
1313
+ #{config_detect}
1314
+ . "$REACT_NATIVE_PATH/scripts/xcode/with-environment.sh"
1315
+ "$NODE_BINARY" "#{script_path}" \\
1316
+ -c "$CONFIG" \\
1317
+ -m "#{product_name}" \\
1318
+ -x "#{xcframeworks_dir}" \\
1319
+ #{shared_args}
1320
+ SH
1321
+ end
1322
+ end
1323
+
1324
+ # '--shared "<Name>:<source_base>"' tokens for shared SPM deps this pod owns.
1325
+ # Non-owners reach the framework via FRAMEWORK_SEARCH_PATHS at link time.
1326
+ def shared_dep_switch_args(pod_name, pod_info)
1327
+ return [] unless pod_info && pod_info[:spm_dependency_frameworks]
1328
+ @framework_owner_map ||= {}
1329
+ pod_info[:spm_dependency_frameworks].filter_map do |dep_name|
1330
+ next nil unless @framework_owner_map[dep_name] == pod_name
1331
+ source_base = shared_spm_dep_source_base(dep_name, pod_info)
1332
+ next nil unless source_base
1333
+ %(--shared "#{dep_name}:#{source_base.gsub(/[\\"$`]/) { |c| "\\#{c}" }}")
1334
+ end
1260
1335
  end
1261
1336
 
1262
1337
  # Returns the shell script for the dSYM source map resolution phase.
@@ -1853,6 +1928,43 @@ module Expo
1853
1928
  own_resolution
1854
1929
  end
1855
1930
 
1931
+ # Candidate parent dirs (each holds <flavor>/<Name>.xcframework subtrees) for
1932
+ # a shared SPM dep. Ordered: EXPO_PRECOMPILED_MODULES_PATH override, monorepo
1933
+ # .spm-deps, then the consumer-side npm-bundled location.
1934
+ def shared_spm_dep_source_base_candidates(dep_name, pod_info)
1935
+ candidates = []
1936
+ candidates << File.join(custom_modules_path, SHARED_SPM_DEPS_SOURCE_DIR, dep_name) if custom_modules_path
1937
+ candidates << File.join(memoized_repo_root, 'packages', 'precompile', PRECOMPILE_BUILD_DIR, SHARED_SPM_DEPS_SOURCE_DIR, dep_name) if memoized_repo_root
1938
+ candidates << File.join(pod_info[:package_root], BUNDLED_SHARED_SPM_DEPS_SUBPATH, dep_name) if pod_info && pod_info[:package_root]
1939
+ candidates
1940
+ end
1941
+
1942
+ # First candidate base that has at least one flavor on disk (used to build switch-script source_base args).
1943
+ def shared_spm_dep_source_base(dep_name, pod_info)
1944
+ shared_spm_dep_source_base_candidates(dep_name, pod_info).find do |base|
1945
+ %w[debug release].any? { |f| File.directory?(File.join(base, f, "#{dep_name}.xcframework")) }
1946
+ end
1947
+ end
1948
+
1949
+ # First candidate that has the requested flavor on disk (walks all candidates so a partial monorepo doesn't shadow a complete npm bundle).
1950
+ def shared_spm_dep_xcframework_path(dep_name, pod_info, flavor)
1951
+ shared_spm_dep_source_base_candidates(dep_name, pod_info).each do |base|
1952
+ path = File.join(base, flavor, "#{dep_name}.xcframework")
1953
+ return path if File.directory?(path)
1954
+ end
1955
+ nil
1956
+ end
1957
+
1958
+ def memoized_repo_root
1959
+ return @repo_root if @memoized_repo_root_set
1960
+ begin
1961
+ @repo_root = find_repo_root
1962
+ ensure
1963
+ @memoized_repo_root_set = true
1964
+ end
1965
+ @repo_root
1966
+ end
1967
+
1856
1968
  def resolve_prebuilt_tarball(pod_info, product_name, flavor, pod_name = nil)
1857
1969
  tarball = File.join(pod_info[:build_output_dir], flavor, 'xcframeworks', "#{product_name}.tar.gz")
1858
1970
  return tarball if File.exist?(tarball)
@@ -2,52 +2,32 @@
2
2
  /**
3
3
  * Replace XCFramework for Debug/Release Configuration
4
4
  *
5
- * This script extracts the correct flavor tarball to switch between debug and release
6
- * xcframeworks. It's invoked from a CocoaPods script_phase before each compile
7
- * to ensure the correct XCFramework variant is linked.
5
+ * Per-pod product swap: extracts <xcframeworksDir>/artifacts/<module>-<config>.tar.gz
6
+ * over <Product>.xcframework, gated on <xcframeworksDir>/artifacts/.last_build_configuration.
7
+ * Sibling shared-dep symlinks under <xcframeworksDir>/ are preserved (only the product
8
+ * xcframework is wiped before re-extracting).
8
9
  *
9
- * Directory structure:
10
- * <xcframeworks_dir>/
11
- * artifacts/
12
- * <Product>-debug.tar.gz (tarball, source of truth)
13
- * <Product>-release.tar.gz (tarball, source of truth)
14
- * .last_build_configuration
15
- * <Product>.xcframework/ (real dir, extracted from tarball)
16
- * <Dependency>.xcframework/ (real dir, if any, extracted from same tarball)
10
+ * Shared-dep repoint (each --shared entry): atomically replaces
11
+ * <xcframeworksDir>/<Name>.xcframework with a symlink to <source_base>/<config>/<Name>.xcframework
12
+ * and writes <xcframeworksDir>/artifacts/<Name>.last_config. The owner pod (decided at pod
13
+ * install time by ensure_shared_spm_deps) receives --shared args for each dep it owns.
17
14
  *
18
15
  * Usage:
19
- * node replace-xcframework.js -c <CONFIG> -m <MODULE_NAME> -x <XCFRAMEWORKS_DIR>
20
- *
21
- * Arguments:
22
- * -c, --config Build configuration: "debug" or "release"
23
- * -m, --module Module/product name (used for tarball lookup and logging)
24
- * -x, --xcframeworks Path to the pod directory (Pods/<PodName>/)
25
- *
26
- * The script:
27
- * 1. Finds the tarball: <xcframeworksDir>/artifacts/<module>-<config>.tar.gz
28
- * 2. Checks artifacts/.last_build_configuration — skips if unchanged
29
- * 3. Removes all *.xcframework directories in xcframeworksDir
30
- * 4. Extracts the tarball: tar -xzf ... -C <xcframeworksDir>
31
- * 5. Writes the new config to artifacts/.last_build_configuration
16
+ * node replace-xcframework.js -c <CONFIG> -m <MODULE> -x <XCFRAMEWORKS_DIR>
17
+ * [--shared <Name>:<source_base>]...
32
18
  *
33
19
  * Based on React Native's replace-rncore-version.js pattern.
34
20
  */
35
21
 
22
+ const { spawnSync } = require('child_process');
36
23
  const fs = require('fs');
37
24
  const path = require('path');
38
- const { spawnSync } = require('child_process');
39
25
 
40
26
  const LOG_PREFIX = '[Expo XCFramework]';
41
27
 
42
- // Parse command line arguments
43
28
  function parseArgs() {
44
29
  const args = process.argv.slice(2);
45
- const result = {
46
- config: null,
47
- module: null,
48
- xcframeworksDir: null,
49
- };
50
-
30
+ const result = { config: null, module: null, xcframeworksDir: null, sharedDeps: [] };
51
31
  for (let i = 0; i < args.length; i++) {
52
32
  switch (args[i]) {
53
33
  case '-c':
@@ -62,116 +42,120 @@ function parseArgs() {
62
42
  case '--xcframeworks':
63
43
  result.xcframeworksDir = args[++i];
64
44
  break;
45
+ case '-s':
46
+ case '--shared': {
47
+ const spec = args[++i] || '';
48
+ const colon = spec.indexOf(':');
49
+ if (colon === -1) {
50
+ console.error(`${LOG_PREFIX} Invalid --shared (expected "<Name>:<source_base>"): ${spec}`);
51
+ process.exit(1);
52
+ }
53
+ result.sharedDeps.push({ name: spec.slice(0, colon), sourceBase: spec.slice(colon + 1) });
54
+ break;
55
+ }
65
56
  }
66
57
  }
67
-
68
58
  return result;
69
59
  }
70
60
 
71
- function main() {
72
- const args = parseArgs();
73
-
74
- // Validate arguments
75
- if (!args.config || !args.module || !args.xcframeworksDir) {
76
- console.error(
77
- 'Usage: replace-xcframework.js -c <CONFIG> -m <MODULE_NAME> -x <XCFRAMEWORKS_DIR>'
78
- );
79
- console.error(' -c, --config Build configuration: "debug" or "release"');
80
- console.error(' -m, --module Module/product name');
81
- console.error(' -x, --xcframeworks Path to the xcframeworks directory');
82
- process.exit(1);
83
- }
84
-
85
- // Normalize config to lowercase
86
- const configLower = args.config.toLowerCase();
87
- if (configLower !== 'debug' && configLower !== 'release') {
88
- console.error(
89
- `${LOG_PREFIX} Invalid configuration: ${args.config}. Must be "debug" or "release".`
90
- );
91
- process.exit(1);
61
+ function readState(file) {
62
+ try {
63
+ return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : null;
64
+ } catch {
65
+ return null;
92
66
  }
67
+ }
93
68
 
94
- const xcframeworksDir = args.xcframeworksDir;
95
- const moduleName = args.module;
96
-
97
- // Validate xcframeworksDir exists
69
+ function processPerPodSwap(args, configLower) {
70
+ const { xcframeworksDir, module: moduleName } = args;
98
71
  if (!fs.existsSync(xcframeworksDir) || !fs.statSync(xcframeworksDir).isDirectory()) {
99
72
  console.error(`${LOG_PREFIX} ${moduleName}: Directory not found: ${xcframeworksDir}`);
100
73
  process.exit(1);
101
74
  }
102
75
 
103
- // Ensure artifacts directory exists
104
76
  const artifactsDir = path.join(xcframeworksDir, 'artifacts');
105
77
  fs.mkdirSync(artifactsDir, { recursive: true });
106
-
107
- // Find the tarball for the requested configuration (stored in artifacts/)
108
78
  const tarballPath = path.join(artifactsDir, `${moduleName}-${configLower}.tar.gz`);
109
79
  const lastConfigFile = path.join(artifactsDir, '.last_build_configuration');
110
80
 
111
- // Check if tarball exists
112
81
  if (!fs.existsSync(tarballPath)) {
113
- console.error(
114
- `${LOG_PREFIX} ${moduleName}: Tarball not found at ${tarballPath}, skipping.`
115
- );
82
+ console.error(`${LOG_PREFIX} ${moduleName}: Tarball not found at ${tarballPath}, skipping.`);
116
83
  return;
117
84
  }
118
85
 
119
- // Read last build configuration
120
- let lastConfig = null;
121
- if (fs.existsSync(lastConfigFile)) {
122
- try {
123
- lastConfig = fs.readFileSync(lastConfigFile, 'utf8').trim();
124
- } catch (e) {
125
- // Ignore read errors — will proceed with extraction
126
- }
127
- }
128
-
129
- // Check if configuration has changed
86
+ const lastConfig = readState(lastConfigFile);
130
87
  if (lastConfig === configLower) {
131
88
  console.log(`${LOG_PREFIX} ${moduleName}: Already extracted ${configLower}, skipping.`);
132
89
  return;
133
90
  }
134
91
 
135
- // Remove all existing *.xcframework directories
136
- const entries = fs.readdirSync(xcframeworksDir);
137
- for (const entry of entries) {
138
- if (!entry.endsWith('.xcframework')) continue;
139
- const entryPath = path.join(xcframeworksDir, entry);
140
-
141
- try {
142
- const stat = fs.lstatSync(entryPath);
143
- if (stat.isDirectory() || stat.isSymbolicLink()) {
144
- fs.rmSync(entryPath, { recursive: true, force: true });
145
- }
146
- } catch (e) {
147
- console.error(`${LOG_PREFIX} ${moduleName}: Warning: failed to remove ${entry}: ${e.message}`);
92
+ // Only remove the product xcframework — shared-dep symlinks staged by
93
+ // ensure_shared_spm_deps are repointed separately below via --shared.
94
+ const productXcfw = path.join(xcframeworksDir, `${moduleName}.xcframework`);
95
+ try {
96
+ fs.rmSync(productXcfw, { recursive: true, force: true });
97
+ } catch (e) {
98
+ if (e.code !== 'ENOENT') {
99
+ console.error(`${LOG_PREFIX} ${moduleName}: failed to remove product xcframework: ${e.message}`);
148
100
  }
149
101
  }
150
102
 
151
- // Extract the tarball using spawnSync to avoid shell interpretation of paths
152
- const result = spawnSync('tar', ['-xzf', tarballPath, '-C', xcframeworksDir], {
153
- stdio: 'pipe',
154
- });
155
-
103
+ const result = spawnSync('tar', ['-xzf', tarballPath, '-C', xcframeworksDir], { stdio: 'pipe' });
156
104
  if (result.status !== 0) {
157
- const stderr = result.stderr ? result.stderr.toString().trim() : 'unknown error';
158
- console.error(`${LOG_PREFIX} ${moduleName}: Failed to extract tarball: ${stderr}`);
105
+ console.error(`${LOG_PREFIX} ${moduleName}: tar failed: ${result.stderr?.toString().trim()}`);
159
106
  process.exit(1);
160
107
  }
161
108
 
162
- // Write last build configuration
163
- try {
164
- fs.writeFileSync(lastConfigFile, configLower);
165
- } catch (e) {
166
- console.error(`${LOG_PREFIX} ${moduleName}: Warning: failed to write config file: ${e.message}`);
109
+ fs.writeFileSync(lastConfigFile, configLower);
110
+ console.log(
111
+ lastConfig
112
+ ? `${LOG_PREFIX} ${moduleName}: Switched from ${lastConfig} to ${configLower}.`
113
+ : `${LOG_PREFIX} ${moduleName}: Extracted ${configLower} tarball.`
114
+ );
115
+ }
116
+
117
+ function repointSharedDep(xcframeworksDir, name, sourceBase, configLower) {
118
+ const artifactsDir = path.join(xcframeworksDir, 'artifacts');
119
+ fs.mkdirSync(artifactsDir, { recursive: true });
120
+ const stateFile = path.join(artifactsDir, `${name}.last_config`);
121
+ const linkPath = path.join(xcframeworksDir, `${name}.xcframework`);
122
+
123
+ // Trust the state file only when the symlink is still present — an externally
124
+ // deleted symlink (e.g. clear_cocoapods_cache wiping the pod dir) must trigger
125
+ // a re-link even if the state file claims the right config.
126
+ if (readState(stateFile) === configLower && fs.existsSync(linkPath)) return;
127
+
128
+ const target = path.join(sourceBase, configLower, `${name}.xcframework`);
129
+ if (!fs.existsSync(target)) {
130
+ console.error(
131
+ `${LOG_PREFIX} Shared dep ${name}: target not found at ${target}. Run the precompile prebuild pipeline for the ${configLower} flavor, or ensure prebuilds/spm-deps/${name}/${configLower}/${name}.xcframework ships with the consuming package.`
132
+ );
133
+ process.exit(1);
167
134
  }
168
135
 
169
- if (lastConfig && lastConfig !== configLower) {
170
- console.log(
171
- `${LOG_PREFIX} ${moduleName}: Switched from ${lastConfig} to ${configLower} (extracted tarball).`
136
+ fs.rmSync(linkPath, { recursive: true, force: true });
137
+ fs.symlinkSync(target, linkPath);
138
+ fs.writeFileSync(stateFile, configLower);
139
+ console.log(`${LOG_PREFIX} Shared dep ${name}: repointed to ${configLower} (${target}).`);
140
+ }
141
+
142
+ function main() {
143
+ const args = parseArgs();
144
+ if (!args.config || !args.module || !args.xcframeworksDir) {
145
+ console.error(
146
+ 'Usage: replace-xcframework.js -c <CONFIG> -m <MODULE> -x <XCFRAMEWORKS_DIR> [--shared <Name>:<source_base>]...'
172
147
  );
173
- } else {
174
- console.log(`${LOG_PREFIX} ${moduleName}: Extracted ${configLower} tarball.`);
148
+ process.exit(1);
149
+ }
150
+ const configLower = args.config.toLowerCase();
151
+ if (configLower !== 'debug' && configLower !== 'release') {
152
+ console.error(`${LOG_PREFIX} Invalid configuration: ${args.config}. Must be "debug" or "release".`);
153
+ process.exit(1);
154
+ }
155
+
156
+ processPerPodSwap(args, configLower);
157
+ for (const dep of args.sharedDeps) {
158
+ repointSharedDep(args.xcframeworksDir, dep.name, dep.sourceBase, configLower);
175
159
  }
176
160
  }
177
161