expo-modules-autolinking 56.0.3 → 56.0.5

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,16 @@
10
10
 
11
11
  ### 💡 Others
12
12
 
13
+ ## 56.0.5 — 2026-05-13
14
+
15
+ ### 🐛 Bug fixes
16
+
17
+ [iOS] Added fallback to source for missing framework slice. ([#45664](https://github.com/expo/expo/pull/45664) by [@chrfalch](https://github.com/chrfalch))
18
+
19
+ ## 56.0.4 — 2026-05-11
20
+
21
+ _This version does not introduce any user-facing changes._
22
+
13
23
  ## 56.0.3 — 2026-05-08
14
24
 
15
25
  ### 🐛 Bug fixes
@@ -34,18 +34,27 @@ fun Project.defineDefaultProperties(versionCatalogs: Optional<VersionCatalog>) {
34
34
  val kotlin = extra.setIfNotExist("kotlinVersion") { versionCatalogs.getVersionOrDefault("kotlin", "2.0.21") }
35
35
  val ksp = extra.setIfNotExist("kspVersion") {
36
36
  versionCatalogs.getVersionOrDefault("ksp") {
37
- try {
38
- return@getVersionOrDefault KSPLookup.getValue(extra.get("kotlinVersion") as String)
39
- } catch (e: Throwable) {
40
- throw IllegalStateException(
41
- "Can't find KSP version for Kotlin version '${extra.get("kotlinVersion")}'. You're probably using an unsupported version of Kotlin. Supported versions are: '${KSPLookup.keys.joinToString(", ")}'",
42
- e
43
- )
37
+ val kotlinVersion = extra.get("kotlinVersion") as String
38
+
39
+ KSPLookup[kotlinVersion]?.let { return@getVersionOrDefault it }
40
+ if (kotlinVersion >= "2.3.0") {
41
+ return@getVersionOrDefault latestKspVersion
44
42
  }
43
+
44
+ val minSupported = KSPLookup.keys.min()
45
+ throw IllegalStateException(
46
+ """
47
+ Kotlin $kotlinVersion is not supported by Expo modules.
48
+ The minimum supported Kotlin version is $minSupported.
49
+ Update 'kotlinVersion' in your project's build.gradle to a supported version.
50
+ Alternatively, you can set 'kspVersion' explicitly in build.gradle to bypass this check, but this is unsupported and may cause build failures.
51
+ """.trimIndent()
52
+ )
45
53
  }
46
54
  }
47
55
 
48
- project.logger.quiet("""
56
+ project.logger.quiet(
57
+ """
49
58
  ${"[ExpoRootProject]".withColor(Colors.GREEN)} Using the following versions:
50
59
  - buildTools: ${buildTools.withColor(Colors.GREEN)}
51
60
  - minSdk: ${minSdk.withColor(Colors.GREEN)}
@@ -54,7 +63,8 @@ fun Project.defineDefaultProperties(versionCatalogs: Optional<VersionCatalog>) {
54
63
  - ndk: ${ndk.withColor(Colors.GREEN)}
55
64
  - kotlin: ${kotlin.withColor(Colors.GREEN)}
56
65
  - ksp: ${ksp.withColor(Colors.GREEN)}
57
- """.trimIndent())
66
+ """.trimIndent()
67
+ )
58
68
  }
59
69
 
60
70
  inline fun ExtraPropertiesExtension.setIfNotExist(name: String, value: () -> Any): Any? {
@@ -1,20 +1,15 @@
1
1
  // Copyright 2015-present 650 Industries. All rights reserved.
2
- // Generated using './scripts/generateKSPLookUp.js'
3
2
  package expo.modules.plugin
4
3
 
4
+ // Starting with KSP 2.3.0, KSP is no longer tied to a specific Kotlin version.
5
+ const val latestKspVersion = "2.3.7"
6
+
7
+ // For older Kotlin versions, KSP releases were tied to specific Kotlin versions.
5
8
  val KSPLookup = mapOf(
6
9
  "2.2.21" to "2.2.21-2.0.5",
7
- "2.3.1" to "2.3.1",
8
- "2.3.0" to "2.3.0",
9
10
  "2.2.20" to "2.2.20-2.0.4",
10
11
  "2.2.10" to "2.2.10-2.0.2",
11
12
  "2.2.0" to "2.2.0-2.0.2",
12
13
  "2.1.21" to "2.1.21-2.0.2",
13
- "2.1.20" to "2.1.20-2.0.1",
14
- "2.1.10" to "2.1.10-1.0.31",
15
- "2.1.0" to "2.1.0-1.0.29",
16
- "2.0.21" to "2.0.21-1.0.28",
17
- "2.0.20" to "2.0.20-1.0.25",
18
- "2.0.10" to "2.0.10-1.0.24",
19
- "2.0.0" to "2.0.0-1.0.24"
14
+ "2.1.20" to "2.1.20-2.0.1"
20
15
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-modules-autolinking",
3
- "version": "56.0.3",
3
+ "version": "56.0.5",
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": "a30353e69ca0d72b9fac5830abc631feda1ba3ae",
52
+ "gitHead": "40f0a6f6711d93762e0506b37e6e077e4bd9a541",
53
53
  "scripts": {
54
54
  "build": "expo-module build",
55
55
  "clean": "expo-module clean",
@@ -22,16 +22,18 @@ module Expo
22
22
 
23
23
  validate_target_definition()
24
24
 
25
- # Clear stale CocoaPods download cache for precompiled pods.
26
- Expo::PrecompiledModules.clear_cocoapods_cache
27
-
28
25
  resolve_result = resolve()
29
26
 
30
27
  Expo::PackagesConfig.instance.coreFeatures = resolve_result['coreFeatures']
31
28
 
32
- # Pass buildFromSource configuration to PrecompiledModules
33
29
  configuration = resolve_result['configuration'] || {}
34
- Expo::PrecompiledModules.build_from_source = configuration['buildFromSource'] || []
30
+ Expo::PrecompiledModules.configure(
31
+ target_platform: @target_definition.platform,
32
+ build_from_source: configuration['buildFromSource'] || []
33
+ )
34
+
35
+ # Clear stale CocoaPods download cache for precompiled pods.
36
+ Expo::PrecompiledModules.clear_cocoapods_cache
35
37
 
36
38
  @packages = resolve_result['modules'].map { |json_package| Package.new(json_package) }
37
39
  @extraPods = resolve_result['extraDependencies']
@@ -29,7 +29,9 @@
29
29
  require 'fileutils'
30
30
  require 'json'
31
31
  require 'net/http'
32
+ require 'open3'
32
33
  require 'set'
34
+ require 'tempfile'
33
35
  require 'uri'
34
36
 
35
37
  module Expo
@@ -76,6 +78,8 @@ module Expo
76
78
  @framework_owner_map = nil # Hash: framework_name -> owning_pod_name
77
79
  @failed_remote_downloads = Set.new
78
80
  @warned_no_prebuilt_react = false
81
+ @target_platform = nil
82
+ @xcframework_slice_cache = nil
79
83
 
80
84
  class << self
81
85
  # Returns the build flavor (debug/release) for precompiled modules.
@@ -113,13 +117,26 @@ module Expo
113
117
  false
114
118
  end
115
119
 
116
- # Sets the list of package name patterns that should be built from source
117
- # instead of using precompiled xcframeworks. Patterns are treated as regexes
118
- # (e.g., ".*" for all, "expo-audio" for exact match, "expo-.*" for prefix).
120
+ def configure(target_platform: nil, build_from_source: nil)
121
+ self.target_platform = target_platform unless target_platform.nil?
122
+ self.build_from_source = build_from_source unless build_from_source.nil?
123
+ end
124
+
119
125
  def build_from_source=(patterns)
120
126
  @build_from_source_patterns = (patterns || []).map { |p| Regexp.new("^#{p}$") }
121
127
  end
122
128
 
129
+ def target_platform=(platform)
130
+ normalized = normalize_xcframework_platform(platform)
131
+ return if @target_platform == normalized
132
+
133
+ @target_platform = normalized
134
+ @all_bundled_frameworks = nil
135
+ @claimed_vendored_frameworks = nil
136
+ @framework_owner_map = nil
137
+ @xcframework_slice_cache = nil
138
+ end
139
+
123
140
  # Checks if a pod is configured to be built from source via buildFromSource.
124
141
  # Matches against both the pod name and the npm package name.
125
142
  def build_from_source?(pod_name)
@@ -1785,6 +1802,7 @@ module Expo
1785
1802
  product_name = pod_info[:product_name] || pod_name
1786
1803
  tarball = resolve_prebuilt_tarball(pod_info, product_name, build_flavor, pod_name)
1787
1804
  return { available: false, reason: :missing_tarball, path: tarball } unless File.exist?(tarball)
1805
+ return { available: false, reason: :missing_platform_slice, path: tarball } unless xcframework_supports_target_platform?(tarball)
1788
1806
 
1789
1807
  { available: true, resolved: [pod_info, product_name, tarball] }
1790
1808
  end
@@ -1842,6 +1860,74 @@ module Expo
1842
1860
  remote_tarball
1843
1861
  end
1844
1862
 
1863
+ def normalize_xcframework_platform(platform)
1864
+ name = platform.respond_to?(:name) ? platform.name : platform
1865
+ name = name.string_name if name.respond_to?(:string_name)
1866
+ normalized = name.to_s.downcase
1867
+ normalized == 'osx' ? 'macos' : normalized
1868
+ end
1869
+
1870
+ def xcframework_supports_target_platform?(path)
1871
+ return true unless @target_platform
1872
+
1873
+ @xcframework_slice_cache ||= {}
1874
+ cache_key = [path, @target_platform]
1875
+ return @xcframework_slice_cache[cache_key] if @xcframework_slice_cache.key?(cache_key)
1876
+
1877
+ @xcframework_slice_cache[cache_key] = begin
1878
+ info_plists = File.directory?(path) ? [read_plist(File.join(path, 'Info.plist'))] : read_xcframework_info_plists_from_tarball(path)
1879
+ info_plists.any? && info_plists.all? { |info_plist| info_plist_supports_target_platform?(info_plist) }
1880
+ end
1881
+ end
1882
+
1883
+ def info_plist_supports_target_platform?(info_plist)
1884
+ return false unless info_plist
1885
+
1886
+ available_libraries = info_plist['AvailableLibraries']
1887
+ return false unless available_libraries.is_a?(Array)
1888
+
1889
+ available_libraries.any? do |library|
1890
+ normalize_xcframework_platform(library['SupportedPlatform']) == @target_platform
1891
+ end
1892
+ end
1893
+
1894
+ def read_xcframework_info_plists_from_tarball(tarball)
1895
+ entries_output, status = Open3.capture2e('tar', 'tzf', tarball)
1896
+ unless status.success?
1897
+ Pod::UI.warn "[Expo-precompiled] Failed to inspect #{File.basename(tarball)}: #{entries_output.strip}"
1898
+ return []
1899
+ end
1900
+
1901
+ entries = entries_output.lines.map(&:strip)
1902
+ plist_entries = entries.select { |entry| entry.end_with?('.xcframework/Info.plist') }
1903
+ Pod::UI.warn "[Expo-precompiled] No XCFramework Info.plist found in #{File.basename(tarball)}" if plist_entries.empty?
1904
+
1905
+ plist_entries.filter_map do |entry|
1906
+ plist_data, plist_status = Open3.capture2e('tar', 'xOzf', tarball, entry)
1907
+ unless plist_status.success?
1908
+ Pod::UI.warn "[Expo-precompiled] Failed to extract #{entry} from #{File.basename(tarball)}: #{plist_data.strip}"
1909
+ next
1910
+ end
1911
+
1912
+ Tempfile.create(['expo-xcframework-info', '.plist']) do |file|
1913
+ file.binmode
1914
+ file.write(plist_data)
1915
+ file.flush
1916
+ read_plist(file.path)
1917
+ end
1918
+ end
1919
+ rescue StandardError => e
1920
+ Pod::UI.warn "[Expo-precompiled] Failed to inspect #{File.basename(tarball)}: #{e.message}"
1921
+ []
1922
+ end
1923
+
1924
+ def read_plist(path)
1925
+ Xcodeproj::Plist.read_from_path(path)
1926
+ rescue StandardError => e
1927
+ Pod::UI.warn "[Expo-precompiled] Failed to read #{File.basename(path)}: #{e.message}"
1928
+ nil
1929
+ end
1930
+
1845
1931
  def failed_remote_downloads
1846
1932
  @failed_remote_downloads ||= Set.new
1847
1933
  end
@@ -2120,6 +2206,8 @@ module Expo
2120
2206
  'prebuilt config not found'
2121
2207
  when :missing_tarball
2122
2208
  'prebuilt tarball not found'
2209
+ when :missing_platform_slice
2210
+ "prebuilt xcframework does not contain a slice for #{@target_platform}"
2123
2211
  when :dependency_unavailable
2124
2212
  reason = format_prebuilt_unavailable_reason(reason: info[:dependency_reason], path: info[:dependency_path])
2125
2213
  "dependency #{info[:dependency]} is not using prebuilt: #{reason}"
@@ -1,101 +0,0 @@
1
- #!/usr/bin/env node
2
- const { execSync } = require('child_process');
3
- const fs = require('fs');
4
-
5
- const minKotlinVersion = '2.0.0';
6
- const maxKotlinVersion = '2.3.10';
7
-
8
- const groupId = 'com.google.devtools.ksp';
9
- const artifactId = 'symbol-processing-gradle-plugin';
10
- const path = require('path').resolve(
11
- __dirname,
12
- '../android/expo-gradle-plugin/expo-autolinking-plugin/src/main/kotlin/expo/modules/plugin/KSPLookup.kt'
13
- );
14
-
15
- const numberPerPage = 30;
16
- const githubApiPath = '/repos/google/ksp/releases';
17
-
18
- async function* fetchKSPReleases() {
19
- let currentPage = 1;
20
- while (true) {
21
- const apiPath = `${githubApiPath}?per_page=${numberPerPage}&page=${currentPage}`;
22
- console.log(`Fetching versions from: ${apiPath}...`);
23
-
24
- const output = execSync(`gh api "${apiPath}"`, { encoding: 'utf-8' });
25
- const data = JSON.parse(output);
26
- const versions = data
27
- .map((release) => release.tag_name)
28
- // Filter out release candidates, beta and milestone versions
29
- .filter((version) => !/(rc|beta|m1)/i.test(version));
30
-
31
- yield versions;
32
-
33
- currentPage += 1;
34
- }
35
- }
36
-
37
- async function filterByKotinVersion(generator, minKotlinVersion, maxKotlinVersion) {
38
- let hasMoreData = true;
39
- const result = [];
40
- while (hasMoreData) {
41
- const { value } = await generator.next();
42
- const versions = value.map((version) => {
43
- const [kotlinVersion] = version.split('-');
44
- return { kotlinVersion, version };
45
- });
46
-
47
- // If the version is lower than the minKotlinVersion, we can stop fetching more data
48
- hasMoreData = !versions.some(({ kotlinVersion }) => kotlinVersion < minKotlinVersion);
49
-
50
- result.push(
51
- ...versions.filter(
52
- ({ kotlinVersion }) =>
53
- kotlinVersion >= minKotlinVersion && kotlinVersion <= maxKotlinVersion
54
- )
55
- );
56
- }
57
- return result;
58
- }
59
-
60
- // Group versions by kotlinVersion and return the most recent version for each kotlinVersion
61
- function groupByKotlinVersion(versions) {
62
- const mostRecentVersions = {};
63
-
64
- // Fill the mostRecentVersions object with the newest version of each kotlinVersion
65
- versions.forEach(({ kotlinVersion, version }) => {
66
- if (!mostRecentVersions[kotlinVersion] || mostRecentVersions[kotlinVersion] < version) {
67
- mostRecentVersions[kotlinVersion] = version;
68
- }
69
- });
70
-
71
- // Convert the results object back into an array
72
- return Object.entries(mostRecentVersions).map(([kotlinVersion, version]) => ({
73
- kotlinVersion,
74
- version,
75
- }));
76
- }
77
-
78
- async function generateKSPLookup() {
79
- const versionsGenerator = fetchKSPReleases(groupId, artifactId, minKotlinVersion);
80
- const versions = groupByKotlinVersion(
81
- await filterByKotinVersion(versionsGenerator, minKotlinVersion, maxKotlinVersion)
82
- );
83
-
84
- console.log(`Found stable versions for ${groupId}:${artifactId}:`);
85
- for (const { kotlinVersion, version } of versions) {
86
- console.log(` ${kotlinVersion} => ${version}`);
87
- }
88
-
89
- console.log('Genereating KSP lookup file...');
90
- const fileContent = `// Copyright 2015-present 650 Industries. All rights reserved.
91
- // Generated using './scripts/generateKSPLookUp.js'
92
- package expo.modules.plugin
93
-
94
- val KSPLookup = mapOf(
95
- ${versions.map(({ kotlinVersion, version }) => ` "${kotlinVersion}" to "${version}"`).join(',\n')}
96
- )
97
- `;
98
- fs.writeFileSync(path, fileContent, 'utf-8');
99
- }
100
-
101
- generateKSPLookup();