expo-updates 56.0.24 → 56.0.26
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 +27 -0
- package/android/build.gradle +2 -2
- package/android/src/main/java/expo/modules/updates/EnabledUpdatesController.kt +7 -2
- package/android/src/main/java/expo/modules/updates/UpdatesUtils.kt +11 -0
- package/android/src/main/java/expo/modules/updates/db/Reaper.kt +16 -1
- package/android/src/main/java/expo/modules/updates/launcher/DatabaseLauncher.kt +10 -1
- package/android/src/main/java/expo/modules/updates/loader/FileDownloader.kt +6 -0
- package/android/src/main/java/expo/modules/updates/loader/Loader.kt +43 -22
- package/android/src/main/java/expo/modules/updates/logging/UpdatesLogEntry.kt +5 -2
- package/android/src/main/java/expo/modules/updates/logging/UpdatesLogReader.kt +1 -1
- package/android/src/main/java/expo/modules/updates/manifest/ExpoUpdatesUpdate.kt +35 -18
- package/ios/EXUpdates/AppLauncher/AppLauncherWithDatabase.swift +10 -2
- package/ios/EXUpdates/AppLoader/EmbeddedAppLoader.swift +69 -7
- package/ios/EXUpdates/AppLoader/RemoteAppLoader.swift +7 -0
- package/ios/EXUpdates/Database/UpdatesDatabase.swift +25 -5
- package/ios/EXUpdates/Database/UpdatesReaper.swift +4 -0
- package/ios/EXUpdates/Update/Update.swift +15 -1
- package/ios/EXUpdates/UpdatesError.swift +3 -0
- package/ios/EXUpdates/UpdatesUtils.swift +25 -0
- package/ios/EXUpdates.podspec +11 -0
- package/package.json +6 -6
- package/utils/src/createManifestForBuildAsync.ts +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,33 @@
|
|
|
10
10
|
|
|
11
11
|
### 💡 Others
|
|
12
12
|
|
|
13
|
+
## 56.0.26 — 2026-08-28
|
|
14
|
+
|
|
15
|
+
### 🐛 Bug fixes
|
|
16
|
+
|
|
17
|
+
- [Android] Register the embedded update in a single transaction. An interrupted registration previously left an update row with no launch asset, which is treated as launchable and then fails every cold start with "Launch asset not found for update"; it now leaves no row, so the next launch registers it cleanly. ([#49130](https://github.com/expo/expo/pull/49130) by [@gwdp](https://github.com/gwdp))
|
|
18
|
+
- [Android] Widen `UpdatesLogEntry.create`'s catch from `JSONException` to `Exception` so log-line parse failures consistently degrade to "skip the entry" instead of propagating. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
19
|
+
- [Android] Correct `UpdatesLogReader.ONE_DAY_MILLISECONDS` from `86400` (seconds) to `86_400_000` (milliseconds), so the "older than one day" purge filter actually retains a day's worth of entries instead of ~86 seconds' worth. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
20
|
+
- [iOS] Propagate database failures from `addNewAssets` instead of swallowing them, which let an update be marked ready without its launch asset and then fail every launch. ([#49456](https://github.com/expo/expo/pull/49456) by [@alanjhughes](https://github.com/alanjhughes))
|
|
21
|
+
- [iOS] Repair ready updates that are missing their launch asset by demoting them to pending during launcher selection, so a corrupted row is skipped and retried instead of failing every launch. ([#49457](https://github.com/expo/expo/pull/49457) by [@alanjhughes](https://github.com/alanjhughes))
|
|
22
|
+
- [Android] Skip and repair updates that are missing their launch asset instead of selecting them for launch, which previously failed every cold start with "Launch asset not found for update". ([#49470](https://github.com/expo/expo/pull/49470) by [@alanjhughes](https://github.com/alanjhughes), based on [#48733](https://github.com/expo/expo/pull/48733) by [@martintreurnicht](https://github.com/martintreurnicht))
|
|
23
|
+
- [iOS] Fix the embedded manifest recording the wrong `packagerHash` for assets that ship scale variants iOS does not allow (such as `@1.5x` and `@4x`): the hashes were read by the filtered scale index instead of the asset's own, so those images resolved to an empty URI and rendered blank in release builds. ([#48811](https://github.com/expo/expo/pull/48811) by [@expo-bot](https://github.com/expo-bot))
|
|
24
|
+
|
|
25
|
+
### 💡 Others
|
|
26
|
+
|
|
27
|
+
- [Android] Replace the "this should never happen" wording in the missing launch asset error with the likely cause and how the state resolves. ([#49130](https://github.com/expo/expo/pull/49130) by [@gwdp](https://github.com/gwdp))
|
|
28
|
+
- [Android] Log purge completion errors via `android.util.Log.e` directly instead of `logger.error`, so the failure path doesn't re-enter the `PersistentFileLog` dispatch queue from inside one of its own tasks. ([#46182](https://github.com/expo/expo/pull/46182) by [@jakequade-pc](https://github.com/jakequade-pc))
|
|
29
|
+
|
|
30
|
+
## 56.0.25 — 2026-08-17
|
|
31
|
+
|
|
32
|
+
### 🐛 Bug fixes
|
|
33
|
+
|
|
34
|
+
- Reject updates whose asset key or file extension contains a path separator, which previously let a manifest write and delete files outside the updates directory. ([#48762](https://github.com/expo/expo/pull/48762), [#48763](https://github.com/expo/expo/pull/48763) by [@alanjhughes](https://github.com/alanjhughes))
|
|
35
|
+
|
|
36
|
+
### 💡 Others
|
|
37
|
+
|
|
38
|
+
- [iOS] Link `libc++` in the test spec so the unit test bundle resolves the C++ symbols it pulls from `ExpoModulesCore`. ([#48762](https://github.com/expo/expo/pull/48762) by [@alanjhughes](https://github.com/alanjhughes))
|
|
39
|
+
|
|
13
40
|
## 56.0.24 — 2026-08-06
|
|
14
41
|
|
|
15
42
|
### 🐛 Bug fixes
|
package/android/build.gradle
CHANGED
|
@@ -42,7 +42,7 @@ expoModule {
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
group = 'host.exp.exponent'
|
|
45
|
-
version = '56.0.
|
|
45
|
+
version = '56.0.26'
|
|
46
46
|
|
|
47
47
|
// Utility method to derive boolean values from the environment or from Java properties,
|
|
48
48
|
// and return them as strings to be used in BuildConfig fields
|
|
@@ -89,7 +89,7 @@ android {
|
|
|
89
89
|
namespace "expo.modules.updates"
|
|
90
90
|
defaultConfig {
|
|
91
91
|
versionCode 31
|
|
92
|
-
versionName '56.0.
|
|
92
|
+
versionName '56.0.26'
|
|
93
93
|
consumerProguardFiles("proguard-rules.pro")
|
|
94
94
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
|
95
95
|
|
|
@@ -4,6 +4,7 @@ import android.app.Activity
|
|
|
4
4
|
import android.content.Context
|
|
5
5
|
import android.net.Uri
|
|
6
6
|
import android.os.Bundle
|
|
7
|
+
import android.util.Log
|
|
7
8
|
import com.facebook.react.ReactHost
|
|
8
9
|
import com.facebook.react.bridge.ReactContext
|
|
9
10
|
import com.facebook.react.devsupport.interfaces.DevSupportManager
|
|
@@ -18,7 +19,6 @@ import expo.modules.updates.events.IUpdatesEventManager
|
|
|
18
19
|
import expo.modules.updates.events.UpdatesEventManager
|
|
19
20
|
import expo.modules.updates.launcher.Launcher.LauncherCallback
|
|
20
21
|
import expo.modules.updates.loader.FileDownloader
|
|
21
|
-
import expo.modules.updates.logging.UpdatesErrorCode
|
|
22
22
|
import expo.modules.updates.logging.UpdatesLogReader
|
|
23
23
|
import expo.modules.updates.logging.UpdatesLogger
|
|
24
24
|
import expo.modules.updates.manifest.EmbeddedManifestUtils
|
|
@@ -90,7 +90,12 @@ class EnabledUpdatesController(
|
|
|
90
90
|
private fun purgeUpdatesLogsOlderThanOneDay() {
|
|
91
91
|
UpdatesLogReader(context.filesDir).purgeLogEntries {
|
|
92
92
|
if (it != null) {
|
|
93
|
-
|
|
93
|
+
// Log directly via android.util.Log rather than through `logger.error`,
|
|
94
|
+
// which writes via the PersistentFileLog dispatch queue. This callback
|
|
95
|
+
// is invoked from inside one of that queue's own tasks, so feeding
|
|
96
|
+
// another entry back into the queue from here re-enters it. Bypassing
|
|
97
|
+
// the queue keeps the failure path off its own back.
|
|
98
|
+
Log.e("expo-updates", "UpdatesLogReader: error in purgeLogEntries", it)
|
|
94
99
|
}
|
|
95
100
|
}
|
|
96
101
|
}
|
|
@@ -166,6 +166,17 @@ object UpdatesUtils {
|
|
|
166
166
|
}
|
|
167
167
|
}
|
|
168
168
|
|
|
169
|
+
/**
|
|
170
|
+
* Asset filenames are built from manifest-controlled values, so a filename holding a path
|
|
171
|
+
* separator or a `..` component would resolve outside the updates directory.
|
|
172
|
+
*/
|
|
173
|
+
fun isSafeFilename(filename: String): Boolean {
|
|
174
|
+
return filename.isNotEmpty() &&
|
|
175
|
+
filename != "." &&
|
|
176
|
+
filename != ".." &&
|
|
177
|
+
filename.none { it == '/' || it == '\\' || it == '\u0000' }
|
|
178
|
+
}
|
|
179
|
+
|
|
169
180
|
fun shouldCheckForUpdateOnLaunch(
|
|
170
181
|
updatesConfiguration: UpdatesConfiguration,
|
|
171
182
|
logger: UpdatesLogger,
|
|
@@ -2,10 +2,12 @@ package expo.modules.updates.db
|
|
|
2
2
|
|
|
3
3
|
import android.util.Log
|
|
4
4
|
import expo.modules.updates.UpdatesConfiguration
|
|
5
|
+
import expo.modules.updates.UpdatesUtils
|
|
5
6
|
import expo.modules.updates.db.entity.AssetEntity
|
|
6
7
|
import expo.modules.updates.db.entity.UpdateEntity
|
|
7
8
|
import expo.modules.updates.manifest.ManifestMetadata
|
|
8
9
|
import expo.modules.updates.selectionpolicy.SelectionPolicy
|
|
10
|
+
import expo.modules.updates.utils.AndroidResourceAssetUtils
|
|
9
11
|
import java.io.File
|
|
10
12
|
|
|
11
13
|
/**
|
|
@@ -47,7 +49,20 @@ object Reaper {
|
|
|
47
49
|
)
|
|
48
50
|
continue
|
|
49
51
|
}
|
|
50
|
-
val
|
|
52
|
+
val relativePath = asset.relativePath
|
|
53
|
+
// Embedded assets are served from the APK, so there is no file here to delete.
|
|
54
|
+
if (relativePath == null || AndroidResourceAssetUtils.isAndroidResourceAsset(relativePath)) {
|
|
55
|
+
continue
|
|
56
|
+
}
|
|
57
|
+
// A row written before asset filenames were validated may point outside the updates directory.
|
|
58
|
+
if (!UpdatesUtils.isSafeFilename(relativePath)) {
|
|
59
|
+
Log.e(
|
|
60
|
+
TAG,
|
|
61
|
+
"Refusing to delete asset with URL " + asset.url + " at unsafe path " + relativePath
|
|
62
|
+
)
|
|
63
|
+
continue
|
|
64
|
+
}
|
|
65
|
+
val path = File(updatesDirectory, relativePath)
|
|
51
66
|
try {
|
|
52
67
|
if (path.exists() && !path.delete()) {
|
|
53
68
|
Log.e(TAG, "Failed to delete asset with URL " + asset.url + " at path " + path.toString())
|
|
@@ -87,7 +87,7 @@ class DatabaseLauncher(
|
|
|
87
87
|
// verify that we have all assets on disk
|
|
88
88
|
// according to the database, we should, but something could have gone wrong on disk
|
|
89
89
|
val launchAsset = database.updateDao().loadLaunchAssetForUpdate(launchedUpdate!!.id)
|
|
90
|
-
?: throw Exception("Launch asset not found for update; this
|
|
90
|
+
?: throw Exception("Launch asset not found for update. The update row has no launch asset; an interrupted registration in an older version of expo-updates can leave an update in this state, and it is replaced once a newer update is downloaded. Debug info: ${launchedUpdate!!.debugInfo()}")
|
|
91
91
|
|
|
92
92
|
if (launchAsset.relativePath == null) {
|
|
93
93
|
throw Exception("Launch asset relative path should not be null. Debug info: ${launchedUpdate!!.debugInfo()}")
|
|
@@ -161,6 +161,15 @@ class DatabaseLauncher(
|
|
|
161
161
|
if (!configuration.hasEmbeddedUpdate && embeddedUpdate?.updateEntity?.id == update.id) {
|
|
162
162
|
continue
|
|
163
163
|
}
|
|
164
|
+
|
|
165
|
+
// An update with no launch asset can never launch. Excluding it here lets the loader
|
|
166
|
+
// re-run and repair the row instead of failing every cold start.
|
|
167
|
+
if (update.status != UpdateStatus.DEVELOPMENT &&
|
|
168
|
+
database.updateDao().loadLaunchAssetForUpdate(update.id) == null
|
|
169
|
+
) {
|
|
170
|
+
logger.warn("Skipping launchable update with no launch asset. Debug info: ${update.debugInfo()}")
|
|
171
|
+
continue
|
|
172
|
+
}
|
|
164
173
|
filteredLaunchableUpdates.add(update)
|
|
165
174
|
}
|
|
166
175
|
val manifestFilters = ManifestMetadata.getManifestFilters(database, configuration)
|
|
@@ -687,6 +687,12 @@ class FileDownloader(
|
|
|
687
687
|
}
|
|
688
688
|
|
|
689
689
|
val filename = UpdatesUtils.createFilenameForAsset(asset)
|
|
690
|
+
if (!UpdatesUtils.isSafeFilename(filename)) {
|
|
691
|
+
val message = "Failed to download asset ${asset.key}"
|
|
692
|
+
val error = IOException("Asset filename \"$filename\" would resolve outside the updates directory")
|
|
693
|
+
logger.error(message, error, UpdatesErrorCode.AssetsFailedToLoad)
|
|
694
|
+
throw IOException(message, error)
|
|
695
|
+
}
|
|
690
696
|
val path = File(destinationDirectory, filename)
|
|
691
697
|
|
|
692
698
|
if (path.exists()) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package expo.modules.updates.loader
|
|
2
2
|
|
|
3
3
|
import android.content.Context
|
|
4
|
+
import androidx.room.withTransaction
|
|
4
5
|
import expo.modules.updates.UpdatesConfiguration
|
|
5
6
|
import expo.modules.updates.UpdatesUtils
|
|
6
7
|
import expo.modules.updates.db.UpdatesDatabase
|
|
@@ -17,6 +18,7 @@ import expo.modules.updates.logging.UpdatesErrorCode
|
|
|
17
18
|
import expo.modules.updates.logging.UpdatesLogger
|
|
18
19
|
import expo.modules.updates.manifest.ManifestMetadata
|
|
19
20
|
import expo.modules.updates.manifest.Update
|
|
21
|
+
import kotlinx.coroutines.CancellationException
|
|
20
22
|
import kotlinx.coroutines.CoroutineScope
|
|
21
23
|
import kotlinx.coroutines.Dispatchers
|
|
22
24
|
import kotlinx.coroutines.SupervisorJob
|
|
@@ -164,21 +166,32 @@ abstract class Loader protected constructor(
|
|
|
164
166
|
database.updateDao().setUpdateScopeKey(existingUpdateEntity, newUpdateEntity.scopeKey)
|
|
165
167
|
}
|
|
166
168
|
|
|
167
|
-
|
|
169
|
+
// A READY update with no launch asset is not actually ready. Fall through to
|
|
170
|
+
// downloadAllAssets so its assets are re-registered instead of staying broken forever.
|
|
171
|
+
if (existingUpdateEntity != null && existingUpdateEntity.status == UpdateStatus.READY &&
|
|
172
|
+
database.updateDao().loadLaunchAssetForUpdate(existingUpdateEntity.id) != null
|
|
173
|
+
) {
|
|
168
174
|
// hooray, we already have this update downloaded and ready to go!
|
|
169
175
|
updateEntity = existingUpdateEntity
|
|
170
176
|
return finish()
|
|
171
177
|
} else {
|
|
178
|
+
val insertUpdateEntityOnFinish: Boolean
|
|
172
179
|
if (existingUpdateEntity == null) {
|
|
173
|
-
// no update already exists with this ID, so we need to
|
|
180
|
+
// no update already exists with this ID, so we need to download everything.
|
|
174
181
|
updateEntity = newUpdateEntity
|
|
175
|
-
|
|
182
|
+
// EMBEDDED is already in the launchable set, so a row inserted before its launch asset exists
|
|
183
|
+
// gets picked as launchable and then fails every launch.
|
|
184
|
+
insertUpdateEntityOnFinish = newUpdateEntity.status == UpdateStatus.EMBEDDED
|
|
185
|
+
if (!insertUpdateEntityOnFinish) {
|
|
186
|
+
database.updateDao().insertUpdate(updateEntity!!)
|
|
187
|
+
}
|
|
176
188
|
} else {
|
|
177
189
|
// we've already partially downloaded the update, so we should use the existing entity.
|
|
178
190
|
// however, it's not ready, so we should try to download all the assets again.
|
|
179
191
|
updateEntity = existingUpdateEntity
|
|
192
|
+
insertUpdateEntityOnFinish = false
|
|
180
193
|
}
|
|
181
|
-
return downloadAllAssets(update)
|
|
194
|
+
return downloadAllAssets(update, insertUpdateEntityOnFinish)
|
|
182
195
|
}
|
|
183
196
|
}
|
|
184
197
|
|
|
@@ -188,7 +201,7 @@ abstract class Loader protected constructor(
|
|
|
188
201
|
ERRORED
|
|
189
202
|
}
|
|
190
203
|
|
|
191
|
-
private suspend fun downloadAllAssets(update: Update): LoaderResult {
|
|
204
|
+
private suspend fun downloadAllAssets(update: Update, insertUpdateEntityOnFinish: Boolean): LoaderResult {
|
|
192
205
|
val assetList = update.assetEntityList.distinctBy { it.key }
|
|
193
206
|
assetTotal = assetList.size
|
|
194
207
|
|
|
@@ -235,26 +248,34 @@ abstract class Loader protected constructor(
|
|
|
235
248
|
assetDownloadJobs.awaitAll()
|
|
236
249
|
|
|
237
250
|
try {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
.
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
251
|
+
database.withTransaction {
|
|
252
|
+
if (insertUpdateEntityOnFinish) {
|
|
253
|
+
database.updateDao().insertUpdate(updateEntity!!)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (asset in existingAssetList) {
|
|
257
|
+
val existingAssetFound = database.assetDao()
|
|
258
|
+
.addExistingAssetToUpdate(updateEntity!!, asset, asset.isLaunchAsset)
|
|
259
|
+
if (!existingAssetFound) {
|
|
260
|
+
// the database and filesystem have gotten out of sync
|
|
261
|
+
// do our best to create a new entry for this file even though it already existed on disk
|
|
262
|
+
// TODO: we should probably get rid of this assumption that if an asset exists on disk with the same filename, it's the same asset
|
|
263
|
+
var hash: ByteArray? = null
|
|
264
|
+
try {
|
|
265
|
+
hash = UpdatesUtils.sha256(File(updatesDirectory, asset.relativePath))
|
|
266
|
+
} catch (_: Exception) {
|
|
267
|
+
}
|
|
268
|
+
asset.downloadTime = Date()
|
|
269
|
+
asset.hash = hash
|
|
270
|
+
finishedAssetList.add(asset)
|
|
249
271
|
}
|
|
250
|
-
asset.downloadTime = Date()
|
|
251
|
-
asset.hash = hash
|
|
252
|
-
finishedAssetList.add(asset)
|
|
253
272
|
}
|
|
254
|
-
}
|
|
255
273
|
|
|
256
|
-
|
|
257
|
-
|
|
274
|
+
database.assetDao().insertAssets(finishedAssetList, updateEntity!!)
|
|
275
|
+
database.updateDao().markUpdateFinished(updateEntity!!)
|
|
276
|
+
}
|
|
277
|
+
} catch (e: CancellationException) {
|
|
278
|
+
throw e
|
|
258
279
|
} catch (e: Exception) {
|
|
259
280
|
throw IOException("Error while adding new update to database", e)
|
|
260
281
|
}
|
|
@@ -3,7 +3,6 @@ package expo.modules.updates.logging
|
|
|
3
3
|
import expo.modules.jsonutils.getNullable
|
|
4
4
|
import expo.modules.jsonutils.require
|
|
5
5
|
import org.json.JSONArray
|
|
6
|
-
import org.json.JSONException
|
|
7
6
|
import org.json.JSONObject
|
|
8
7
|
|
|
9
8
|
/**
|
|
@@ -59,7 +58,11 @@ data class UpdatesLogEntry(
|
|
|
59
58
|
List(jsonArray.length()) { i -> jsonArray.getString(i) }
|
|
60
59
|
}
|
|
61
60
|
)
|
|
62
|
-
} catch (e:
|
|
61
|
+
} catch (e: Exception) {
|
|
62
|
+
// `JSONObject.require<T>` can surface a ClassCastException via a blind
|
|
63
|
+
// cast in the catch-all branch (e.g. an int-sized numeric `duration`
|
|
64
|
+
// token surfaces as `Integer`), which would otherwise escape and
|
|
65
|
+
// propagate to the caller.
|
|
63
66
|
null
|
|
64
67
|
}
|
|
65
68
|
}
|
|
@@ -14,6 +14,7 @@ import expo.modules.updates.db.enums.UpdateStatus
|
|
|
14
14
|
import org.json.JSONArray
|
|
15
15
|
import org.json.JSONException
|
|
16
16
|
import org.json.JSONObject
|
|
17
|
+
import java.io.IOException
|
|
17
18
|
import java.text.ParseException
|
|
18
19
|
import java.util.*
|
|
19
20
|
|
|
@@ -95,27 +96,43 @@ class ExpoUpdatesUpdate private constructor(
|
|
|
95
96
|
companion object {
|
|
96
97
|
private val TAG = Update::class.java.simpleName
|
|
97
98
|
|
|
98
|
-
@Throws(
|
|
99
|
+
@Throws(Exception::class)
|
|
99
100
|
fun fromExpoUpdatesManifest(
|
|
100
101
|
manifest: ExpoUpdatesManifest,
|
|
101
102
|
extensions: JSONObject?,
|
|
102
103
|
configuration: UpdatesConfiguration
|
|
103
|
-
): ExpoUpdatesUpdate
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
104
|
+
): ExpoUpdatesUpdate {
|
|
105
|
+
val update = ExpoUpdatesUpdate(
|
|
106
|
+
manifest,
|
|
107
|
+
id = UUID.fromString(manifest.getID()),
|
|
108
|
+
configuration.scopeKey,
|
|
109
|
+
commitTime = try {
|
|
110
|
+
UpdatesUtils.parseDateString(manifest.getCreatedAt())
|
|
111
|
+
} catch (e: ParseException) {
|
|
112
|
+
Log.e(TAG, "Could not parse manifest createdAt string; falling back to current time", e)
|
|
113
|
+
Date()
|
|
114
|
+
},
|
|
115
|
+
runtimeVersion = manifest.getRuntimeVersion(),
|
|
116
|
+
launchAsset = manifest.getLaunchAsset(),
|
|
117
|
+
assets = manifest.getAssets(),
|
|
118
|
+
extensions = extensions,
|
|
119
|
+
url = configuration.updateUrl,
|
|
120
|
+
requestHeaders = configuration.requestHeaders
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
update.assetEntityList.forEach { asset ->
|
|
124
|
+
val filename = UpdatesUtils.createFilenameForAsset(asset)
|
|
125
|
+
if (!UpdatesUtils.isSafeFilename(filename)) {
|
|
126
|
+
throw IOException(
|
|
127
|
+
"Update ${manifest.getID()} could not be loaded because the asset filename " +
|
|
128
|
+
"\"$filename\" is not a valid filename. Assets are stored under their key and file " +
|
|
129
|
+
"extension, so neither may contain a path separator. Check the server that produced " +
|
|
130
|
+
"this manifest."
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return update
|
|
136
|
+
}
|
|
120
137
|
}
|
|
121
138
|
}
|
|
@@ -32,6 +32,11 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
|
|
|
32
32
|
public var launchAssetUrl: URL?
|
|
33
33
|
public var assetFilesMap: [String: String]?
|
|
34
34
|
|
|
35
|
+
/// Set when the launched update is served directly from the embedded app binary. Tracked
|
|
36
|
+
/// explicitly because `assetFilesMap` is now populated with the bundle-resolved embedded assets,
|
|
37
|
+
/// so it can no longer double as the "using embedded assets" signal.
|
|
38
|
+
private var launchedFromEmbeddedBundle = false
|
|
39
|
+
|
|
35
40
|
private let launcherQueue: DispatchQueue
|
|
36
41
|
private var completedAssets: Int
|
|
37
42
|
private let config: UpdatesConfig
|
|
@@ -54,7 +59,7 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
public func isUsingEmbeddedAssets() -> Bool {
|
|
57
|
-
return
|
|
62
|
+
return launchedFromEmbeddedBundle
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
public static func launchableUpdate(
|
|
@@ -178,7 +183,10 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
|
|
|
178
183
|
}
|
|
179
184
|
|
|
180
185
|
if launchedUpdate.status == UpdateStatus.StatusEmbedded {
|
|
181
|
-
|
|
186
|
+
launchedFromEmbeddedBundle = true
|
|
187
|
+
// The embedded update's assets aren't copied into the cache, so resolve them from the app
|
|
188
|
+
// binary. Populating the map here keeps `Updates.localAssets` available instead of empty.
|
|
189
|
+
assetFilesMap = UpdatesUtils.embeddedAssetsMap(withConfig: config, database: database, logger: logger)
|
|
182
190
|
launchAssetUrl = updatesBundle.url(
|
|
183
191
|
forResource: EmbeddedAppLoader.EXUpdatesBareEmbeddedBundleFilename,
|
|
184
192
|
withExtension: EmbeddedAppLoader.EXUpdatesBareEmbeddedBundleFileType
|
|
@@ -8,14 +8,12 @@
|
|
|
8
8
|
import Foundation
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
* Subclass of AppLoader which
|
|
12
|
-
* expo-updates cache location.
|
|
11
|
+
* Subclass of AppLoader which loads the embedded update.
|
|
13
12
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* redownloaded if included in future updates.
|
|
13
|
+
* By default (`EX_UPDATES_COPY_EMBEDDED_ASSETS` off) the update is registered but its assets aren't
|
|
14
|
+
* read into the database; it keeps StatusEmbedded and launches from the app binary, so first launch
|
|
15
|
+
* stays fast. When the flag is on, assets are read into the database and it launches like any other
|
|
16
|
+
* update, so a later update reusing them avoids a redownload.
|
|
19
17
|
*/
|
|
20
18
|
@objc(EXUpdatesEmbeddedAppLoader)
|
|
21
19
|
@objcMembers
|
|
@@ -29,6 +27,24 @@ public final class EmbeddedAppLoader: AppLoader {
|
|
|
29
27
|
|
|
30
28
|
private static var embeddedManifestInternal: EmbeddedUpdate?
|
|
31
29
|
|
|
30
|
+
/// Whether to read and hash embedded assets into the database on first launch. Defaults to the
|
|
31
|
+
/// build-time `EX_UPDATES_COPY_EMBEDDED_ASSETS` flag (off). Overridable for testing.
|
|
32
|
+
internal var shouldCopyEmbeddedAssets: Bool = UpdatesUtils.shouldCopyEmbeddedAssets()
|
|
33
|
+
|
|
34
|
+
internal let completionQueue: DispatchQueue
|
|
35
|
+
|
|
36
|
+
public required override init(
|
|
37
|
+
config: UpdatesConfig,
|
|
38
|
+
logger: UpdatesLogger,
|
|
39
|
+
database: UpdatesDatabase,
|
|
40
|
+
directory: URL,
|
|
41
|
+
launchedUpdate: Update?,
|
|
42
|
+
completionQueue: DispatchQueue
|
|
43
|
+
) {
|
|
44
|
+
self.completionQueue = completionQueue
|
|
45
|
+
super.init(config: config, logger: logger, database: database, directory: directory, launchedUpdate: launchedUpdate, completionQueue: completionQueue)
|
|
46
|
+
}
|
|
47
|
+
|
|
32
48
|
/**
|
|
33
49
|
Gets the embedded update.
|
|
34
50
|
If the `UpdatesConfig.hasEmbeddedUpdate` is false, it returns nil
|
|
@@ -136,6 +152,18 @@ public final class EmbeddedAppLoader: AppLoader {
|
|
|
136
152
|
self.assetBlock = assetBlock
|
|
137
153
|
self.successBlock = successBlock
|
|
138
154
|
self.errorBlock = errorBlock
|
|
155
|
+
startEmbeddedLoad(fromEmbeddedManifest: embeddedManifest)
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/// Loads the embedded update: with copying off (default) registers it without ingesting assets,
|
|
159
|
+
/// otherwise loads them like any other update. Separate from `loadUpdateResponseFromEmbeddedManifest`
|
|
160
|
+
/// so tests can pass a manifest without reading the bundle.
|
|
161
|
+
internal func startEmbeddedLoad(fromEmbeddedManifest embeddedManifest: Update) {
|
|
162
|
+
if !shouldCopyEmbeddedAssets {
|
|
163
|
+
registerEmbeddedUpdateWithoutCopying(embeddedManifest)
|
|
164
|
+
return
|
|
165
|
+
}
|
|
166
|
+
|
|
139
167
|
startLoading(fromUpdateResponse: UpdateResponse(
|
|
140
168
|
responseHeaderData: nil,
|
|
141
169
|
manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest),
|
|
@@ -143,6 +171,40 @@ public final class EmbeddedAppLoader: AppLoader {
|
|
|
143
171
|
))
|
|
144
172
|
}
|
|
145
173
|
|
|
174
|
+
/// Registers the embedded update without reading, hashing, or copying its assets. A StatusEmbedded
|
|
175
|
+
/// update resolves its bundle and assets from the app binary at launch. Tradeoff: a future update
|
|
176
|
+
/// reusing an embedded asset re-downloads it.
|
|
177
|
+
private func registerEmbeddedUpdateWithoutCopying(_ embeddedManifest: Update) {
|
|
178
|
+
database.databaseQueue.async {
|
|
179
|
+
do {
|
|
180
|
+
let existingUpdate = try? self.database.update(withId: embeddedManifest.updateId, config: self.config)
|
|
181
|
+
if existingUpdate == nil {
|
|
182
|
+
try self.database.addUpdate(embeddedManifest, config: self.config)
|
|
183
|
+
}
|
|
184
|
+
// Mark finished (keep = 1) so the reaper retains it; it stays StatusEmbedded since copying is off.
|
|
185
|
+
try self.database.markUpdateFinished(embeddedManifest)
|
|
186
|
+
} catch {
|
|
187
|
+
let errorBlock = self.errorBlock
|
|
188
|
+
self.completionQueue.async {
|
|
189
|
+
errorBlock?(UpdatesError.appLoaderUnknownError(cause: error))
|
|
190
|
+
self.reset()
|
|
191
|
+
}
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
let successBlock = self.successBlock
|
|
196
|
+
let updateResponse = UpdateResponse(
|
|
197
|
+
responseHeaderData: nil,
|
|
198
|
+
manifestUpdateResponsePart: ManifestUpdateResponsePart(updateManifest: embeddedManifest),
|
|
199
|
+
directiveUpdateResponsePart: nil
|
|
200
|
+
)
|
|
201
|
+
self.completionQueue.async {
|
|
202
|
+
successBlock?(updateResponse)
|
|
203
|
+
self.reset()
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
146
208
|
override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
|
|
147
209
|
FileDownloader.assetFilesQueue.async {
|
|
148
210
|
self.handleAssetDownloadAlreadyExists(asset)
|
|
@@ -105,6 +105,13 @@ public final class RemoteAppLoader: AppLoader {
|
|
|
105
105
|
}
|
|
106
106
|
|
|
107
107
|
override public func downloadAsset(_ asset: UpdateAsset, extraHeaders: [String: Any]) {
|
|
108
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
109
|
+
self.handleAssetDownload(
|
|
110
|
+
withError: UpdatesError.remoteAppLoaderUnsafeAssetFilename(filename: asset.filename),
|
|
111
|
+
asset: asset
|
|
112
|
+
)
|
|
113
|
+
return
|
|
114
|
+
}
|
|
108
115
|
let urlOnDisk = self.directory.appendingPathComponent(asset.filename)
|
|
109
116
|
|
|
110
117
|
let progressBlock = { [weak self] fractionCompleted in
|
|
@@ -160,7 +160,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
160
160
|
)
|
|
161
161
|
} catch {
|
|
162
162
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
163
|
-
|
|
163
|
+
throw error
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// statements must stay in precisely this order for last_insert_rowid() to work correctly
|
|
@@ -170,7 +170,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
170
170
|
_ = try execute(sql: updateSql, withArgs: [updateId])
|
|
171
171
|
} catch {
|
|
172
172
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
173
|
-
|
|
173
|
+
throw error
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
@@ -181,7 +181,7 @@ public final class UpdatesDatabase: NSObject {
|
|
|
181
181
|
_ = try execute(sql: updateInsertSql, withArgs: [updateId])
|
|
182
182
|
} catch {
|
|
183
183
|
sqlite3_exec(db, "ROLLBACK;", nil, nil, nil)
|
|
184
|
-
|
|
184
|
+
throw error
|
|
185
185
|
}
|
|
186
186
|
}
|
|
187
187
|
|
|
@@ -280,7 +280,11 @@ public final class UpdatesDatabase: NSObject {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
public func markUpdateFinished(_ update: Update) throws {
|
|
283
|
-
|
|
283
|
+
// With copying off, embedded updates have no asset rows, so they must stay StatusEmbedded to keep
|
|
284
|
+
// launching from the bundle. Promoting to StatusReady would route launch through the database
|
|
285
|
+
// asset path, which has no rows for them.
|
|
286
|
+
let keepEmbedded = update.status == UpdateStatus.StatusEmbedded && !UpdatesUtils.shouldCopyEmbeddedAssets()
|
|
287
|
+
if update.status != UpdateStatus.StatusDevelopment && !keepEmbedded {
|
|
284
288
|
update.status = UpdateStatus.StatusReady
|
|
285
289
|
}
|
|
286
290
|
|
|
@@ -440,7 +444,23 @@ public final class UpdatesDatabase: NSObject {
|
|
|
440
444
|
)
|
|
441
445
|
|
|
442
446
|
let rows = try execute(sql: sql, withArgs: [config.scopeKey])
|
|
443
|
-
|
|
447
|
+
|
|
448
|
+
// A ready row with no launch asset is corrupt (e.g. from an interrupted registration) and
|
|
449
|
+
// would fail every launch. Demote it for the loader to retry instead of offering it.
|
|
450
|
+
var launchableRows: [[String: Any?]] = []
|
|
451
|
+
for row in rows {
|
|
452
|
+
let status: NSNumber = row.requiredValue(forKey: "status")
|
|
453
|
+
let launchAssetId: NSNumber? = row.optionalValue(forKey: "launch_asset_id")
|
|
454
|
+
if status.intValue == UpdateStatus.StatusReady.rawValue && launchAssetId == nil {
|
|
455
|
+
let updateId: UUID = row.requiredValue(forKey: "id")
|
|
456
|
+
let demoteSql = "UPDATE updates SET status = ?1 WHERE id = ?2;"
|
|
457
|
+
_ = try execute(sql: demoteSql, withArgs: [UpdateStatus.StatusPending.rawValue, updateId])
|
|
458
|
+
} else {
|
|
459
|
+
launchableRows.append(row)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
return launchableRows.map { row in
|
|
444
464
|
update(withRow: row, config: config)
|
|
445
465
|
}
|
|
446
466
|
}
|
|
@@ -71,6 +71,10 @@ public final class UpdatesReaper: NSObject {
|
|
|
71
71
|
|
|
72
72
|
let beginDeleteAssets = Date()
|
|
73
73
|
for asset in assetsForDeletion {
|
|
74
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
75
|
+
logger.warn(message: "Refusing to delete asset at unsafe path \(asset.filename)")
|
|
76
|
+
continue
|
|
77
|
+
}
|
|
74
78
|
let localUrl = directory.appendingPathComponent(asset.filename)
|
|
75
79
|
if FileManager.default.fileExists(atPath: localUrl.path) {
|
|
76
80
|
do {
|
|
@@ -77,6 +77,7 @@ public enum UpdateStatus: Int {
|
|
|
77
77
|
public enum UpdateError: Error, Sendable, LocalizedError {
|
|
78
78
|
case invalidExpoProtocolVersion(protocolVersion: Int)
|
|
79
79
|
case legacyManifestInstantiationInvalid
|
|
80
|
+
case unsafeAssetFilename(updateId: String, filename: String)
|
|
80
81
|
|
|
81
82
|
public var errorDescription: String? {
|
|
82
83
|
switch self {
|
|
@@ -84,6 +85,10 @@ public enum UpdateError: Error, Sendable, LocalizedError {
|
|
|
84
85
|
return "Invalid Expo Updates protocol version: \(protocolVersion)"
|
|
85
86
|
case .legacyManifestInstantiationInvalid:
|
|
86
87
|
return "This version of expo-updates can no longer load legacy manifests"
|
|
88
|
+
case let .unsafeAssetFilename(updateId, filename):
|
|
89
|
+
return "Update \(updateId) could not be loaded because the asset filename \"\(filename)\" " +
|
|
90
|
+
"is not a valid filename. Assets are stored under their key and file extension, so neither " +
|
|
91
|
+
"may contain a path separator. Check the server that produced this manifest."
|
|
87
92
|
}
|
|
88
93
|
}
|
|
89
94
|
}
|
|
@@ -157,12 +162,21 @@ public class Update: NSObject {
|
|
|
157
162
|
}
|
|
158
163
|
switch protocolVersion {
|
|
159
164
|
case 0, 1:
|
|
160
|
-
|
|
165
|
+
let update = ExpoUpdatesUpdate.update(
|
|
161
166
|
withExpoUpdatesManifest: ExpoUpdatesManifest(rawManifestJSON: withManifest),
|
|
162
167
|
extensions: extensions,
|
|
163
168
|
config: config,
|
|
164
169
|
database: database
|
|
165
170
|
)
|
|
171
|
+
try update.assets()?.forEach { asset in
|
|
172
|
+
guard UpdatesUtils.isSafeFilename(asset.filename) else {
|
|
173
|
+
throw UpdateError.unsafeAssetFilename(
|
|
174
|
+
updateId: update.updateId.uuidString,
|
|
175
|
+
filename: asset.filename
|
|
176
|
+
)
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return update
|
|
166
180
|
default:
|
|
167
181
|
throw UpdateError.invalidExpoProtocolVersion(protocolVersion: protocolVersion)
|
|
168
182
|
}
|
|
@@ -26,6 +26,7 @@ public enum UpdatesError: Error, Sendable, LocalizedError {
|
|
|
26
26
|
case fileDownloaderFailedUpdateIDsFailure(cause: Error)
|
|
27
27
|
case fileDownloaderUnknownError(cause: Error)
|
|
28
28
|
case remoteAppLoaderAssetMissingUrl
|
|
29
|
+
case remoteAppLoaderUnsafeAssetFilename(filename: String)
|
|
29
30
|
case remoteAppLoaderHeaderDataError(cause: Error)
|
|
30
31
|
case remoteAppLoaderUnknownError(cause: Error)
|
|
31
32
|
case appLoaderFailedToLoadAllAssets
|
|
@@ -92,6 +93,8 @@ public enum UpdatesError: Error, Sendable, LocalizedError {
|
|
|
92
93
|
return "Unknown error: \(cause.localizedDescription)"
|
|
93
94
|
case .remoteAppLoaderAssetMissingUrl:
|
|
94
95
|
return "Failed to download asset with no URL provided"
|
|
96
|
+
case let .remoteAppLoaderUnsafeAssetFilename(filename):
|
|
97
|
+
return "Failed to download asset: filename \"\(filename)\" would resolve outside the updates directory"
|
|
95
98
|
case let .remoteAppLoaderHeaderDataError(cause):
|
|
96
99
|
return "Error persisting header data to disk: \(cause.localizedDescription)"
|
|
97
100
|
case .appLoaderFailedToLoadAllAssets:
|
|
@@ -75,6 +75,23 @@ public final class UpdatesUtils: NSObject {
|
|
|
75
75
|
return updatesDirectory
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Asset filenames are built from manifest-controlled values, so a filename holding a path
|
|
80
|
+
* separator or a `..` component would resolve outside the updates directory.
|
|
81
|
+
*
|
|
82
|
+
* Matching is done on unicode scalars because `String.contains` compares grapheme clusters, and
|
|
83
|
+
* a separator followed by a combining mark forms one cluster that does not equal the separator.
|
|
84
|
+
* The filesystem still sees the separator byte.
|
|
85
|
+
*/
|
|
86
|
+
public static func isSafeFilename(_ filename: String) -> Bool {
|
|
87
|
+
return !filename.isEmpty &&
|
|
88
|
+
filename != "." &&
|
|
89
|
+
filename != ".." &&
|
|
90
|
+
!filename.unicodeScalars.contains("/") &&
|
|
91
|
+
!filename.unicodeScalars.contains("\\") &&
|
|
92
|
+
!filename.unicodeScalars.contains("\0")
|
|
93
|
+
}
|
|
94
|
+
|
|
78
95
|
// MARK: - Internal methods
|
|
79
96
|
|
|
80
97
|
public static func defaultNativeStateMachineContextJson() -> [String: Any?] {
|
|
@@ -165,6 +182,14 @@ public final class UpdatesUtils: NSObject {
|
|
|
165
182
|
#endif
|
|
166
183
|
}
|
|
167
184
|
|
|
185
|
+
internal static func shouldCopyEmbeddedAssets() -> Bool {
|
|
186
|
+
#if EX_UPDATES_COPY_EMBEDDED_ASSETS
|
|
187
|
+
return true
|
|
188
|
+
#else
|
|
189
|
+
return false
|
|
190
|
+
#endif
|
|
191
|
+
}
|
|
192
|
+
|
|
168
193
|
internal static func runBlockOnMainThread(_ block: @escaping () -> Void) {
|
|
169
194
|
if Thread.isMainThread {
|
|
170
195
|
block()
|
package/ios/EXUpdates.podspec
CHANGED
|
@@ -9,6 +9,9 @@ end
|
|
|
9
9
|
if ENV['EX_UPDATES_CUSTOM_INIT'] != '1'
|
|
10
10
|
ENV['EX_UPDATES_CUSTOM_INIT'] = podfile_properties['updatesCustomInit'] == 'true' ? '1' : '0'
|
|
11
11
|
end
|
|
12
|
+
if ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] != '1'
|
|
13
|
+
ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] = podfile_properties['updatesCopyEmbeddedAssets'] == 'true' ? '1' : '0'
|
|
14
|
+
end
|
|
12
15
|
|
|
13
16
|
use_dev_client = false
|
|
14
17
|
begin
|
|
@@ -72,6 +75,7 @@ Pod::Spec.new do |s|
|
|
|
72
75
|
|
|
73
76
|
ex_updates_native_debug = ENV['EX_UPDATES_NATIVE_DEBUG'] == '1'
|
|
74
77
|
ex_updates_custom_init = ENV['EX_UPDATES_CUSTOM_INIT'] == '1'
|
|
78
|
+
ex_updates_copy_embedded_assets = ENV['EX_UPDATES_COPY_EMBEDDED_ASSETS'] == '1'
|
|
75
79
|
if ex_updates_native_debug
|
|
76
80
|
other_debug_c_flags << ' -DEX_UPDATES_NATIVE_DEBUG=1'
|
|
77
81
|
other_debug_swift_flags << ' -DEX_UPDATES_NATIVE_DEBUG'
|
|
@@ -82,6 +86,12 @@ Pod::Spec.new do |s|
|
|
|
82
86
|
other_release_c_flags << ' -DEX_UPDATES_CUSTOM_INIT=1'
|
|
83
87
|
other_release_swift_flags << ' -DEX_UPDATES_CUSTOM_INIT'
|
|
84
88
|
end
|
|
89
|
+
if ex_updates_copy_embedded_assets
|
|
90
|
+
other_debug_c_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS=1'
|
|
91
|
+
other_debug_swift_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS'
|
|
92
|
+
other_release_c_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS=1'
|
|
93
|
+
other_release_swift_flags << ' -DEX_UPDATES_COPY_EMBEDDED_ASSETS'
|
|
94
|
+
end
|
|
85
95
|
if use_dev_client
|
|
86
96
|
other_debug_c_flags << ' -DUSE_DEV_CLIENT=1'
|
|
87
97
|
other_debug_swift_flags << ' -DUSE_DEV_CLIENT'
|
|
@@ -141,6 +151,7 @@ Pod::Spec.new do |s|
|
|
|
141
151
|
test_spec.dependency 'ExpoModulesTestCore'
|
|
142
152
|
|
|
143
153
|
test_spec.pod_target_xcconfig = {
|
|
154
|
+
'OTHER_LDFLAGS' => '$(inherited) -lc++',
|
|
144
155
|
'USER_HEADER_SEARCH_PATHS' => '"${CONFIGURATION_TEMP_DIR}/EXUpdates.build/DerivedSources"',
|
|
145
156
|
'GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS' => 'YES',
|
|
146
157
|
'GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS' => 'YES',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-updates",
|
|
3
|
-
"version": "56.0.
|
|
3
|
+
"version": "56.0.26",
|
|
4
4
|
"description": "Fetches and manages remotely-hosted assets and updates to your app's JS bundle.",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -60,10 +60,10 @@
|
|
|
60
60
|
"picomatch": "^4.0.4",
|
|
61
61
|
"ts-node": "^10.9.2",
|
|
62
62
|
"xstate": "^4.37.2",
|
|
63
|
-
"expo": "56.0.
|
|
64
|
-
"expo-
|
|
65
|
-
"
|
|
66
|
-
"expo-
|
|
63
|
+
"expo": "56.0.21",
|
|
64
|
+
"@expo/metro-config": "56.0.19",
|
|
65
|
+
"expo-module-scripts": "56.0.3",
|
|
66
|
+
"expo-dev-client": "56.0.26"
|
|
67
67
|
},
|
|
68
68
|
"peerDependencies": {
|
|
69
69
|
"expo": "*",
|
|
@@ -82,7 +82,7 @@
|
|
|
82
82
|
"./scripts/with-node.sh"
|
|
83
83
|
]
|
|
84
84
|
},
|
|
85
|
-
"gitHead": "
|
|
85
|
+
"gitHead": "1b8eb0e13635d3ef52c1c11d305ff927d01390ae",
|
|
86
86
|
"scripts": {
|
|
87
87
|
"build": "expo-module build",
|
|
88
88
|
"clean": "expo-module clean",
|
|
@@ -64,12 +64,14 @@ export async function createManifestForBuildAsync(
|
|
|
64
64
|
'The hashAssetFiles Metro plugin is not configured. You need to add a metro.config.js to your project that configures Metro to use this plugin. See https://github.com/expo/expo/blob/main/packages/expo-updates/README.md#metroconfigjs for an example.'
|
|
65
65
|
);
|
|
66
66
|
}
|
|
67
|
-
filterPlatformAssetScales(platform, asset.scales).forEach(function (scale
|
|
67
|
+
filterPlatformAssetScales(platform, asset.scales).forEach(function (scale) {
|
|
68
68
|
const baseAssetInfoForManifest = {
|
|
69
69
|
name: asset.name,
|
|
70
70
|
type: asset.type,
|
|
71
71
|
scale,
|
|
72
|
-
|
|
72
|
+
// `fileHashes` is parallel to the unfiltered `asset.scales`, so it must be indexed by the
|
|
73
|
+
// scale's position there rather than by its position in the filtered list.
|
|
74
|
+
packagerHash: asset.fileHashes[asset.scales.indexOf(scale)],
|
|
73
75
|
subdirectory: asset.httpServerLocation,
|
|
74
76
|
};
|
|
75
77
|
if (platform === 'ios') {
|