Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
164 changes: 82 additions & 82 deletions apps/bare-expo/ios/Podfile.lock

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions apps/eas-expo-go/eas.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
"EXPO_ROOT_DIR": "/home/expo/workingdir/build",
"SHARP_IGNORE_GLOBAL_LIBVIPS": "1",
"EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP": "1",
"GRADLE_OPTS": "-Xmx16g -XX:MaxMetaspaceSize=1g -Dorg.gradle.vfs.watch=false"
"GRADLE_OPTS": "-Xmx16g -XX:MaxMetaspaceSize=1g -Dorg.gradle.vfs.watch=false",
"EAS_RESTORE_CACHE": "0"
}
},
"ios": {
Expand All @@ -29,7 +30,9 @@
"EAS_BUILD_PLATFORM": "ios",
"EXPO_ROOT_DIR": "/Users/expo/workingdir/build",
"SHARP_IGNORE_GLOBAL_LIBVIPS": "1",
"EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP": "1"
"EAS_BUILD_DISABLE_BUNDLE_JAVASCRIPT_STEP": "1",
"EAS_RESTORE_CACHE": "0",
"USE_CCACHE": "0"
}
}
},
Expand Down
4 changes: 4 additions & 0 deletions apps/eas-expo-go/scripts/eas-build-pre-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ fi

pushd $ROOT_DIR/tools

# Install just the `tools` workspace and its deps so `et` can run here. The
# root `pnpm install` runs in a later EAS phase.
pnpm install --filter expotools... --ignore-scripts --dir "$ROOT_DIR"

if [ "$EAS_BUILD_PROFILE" = "release-client" ] && [ "$EAS_BUILD_PLATFORM" = "ios" ]; then
et eas remove-background-permissions-from-info-plist
fi
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ import host.exp.exponent.kernel.ExpoViewKernel
import host.exp.exponent.kernel.Kernel
import host.exp.exponent.kernel.KernelConfig
import host.exp.exponent.storage.ExponentSharedPreferences
import host.exp.exponent.utils.HermesBundleUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.File
import java.net.URI
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
Expand Down Expand Up @@ -263,7 +265,16 @@ class ExpoUpdatesAppLoader @JvmOverloads constructor(

// ReactAndroid will load the bundle on its own in development mode
if (!manifest.isDevelopmentMode()) {
callback.onBundleCompleted(launcher.launchAssetFile!!)
val launchAssetFile = launcher.launchAssetFile!!
val isEasUpdate = runCatching { URI(manifestUrl).host }.getOrNull() == "u.expo.dev"
if (!isEasUpdate && HermesBundleUtils.isHermesBundle(File(launchAssetFile))) {
val errorJson = JSONObject().apply {
put("errorCode", "EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED")
put("message", "Hermes bytecode bundle is not supported by Expo Go")
}
throw ManifestException(null, manifestUrl, errorJson)
}
callback.onBundleCompleted(launchAssetFile)
}
} catch (e: Exception) {
callback.onError(e)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ class ManifestException : ExponentException {
}
"EXPERIENCE_SDK_VERSION_TOO_NEW" -> "Project is incompatible with this version of Expo Go"
"SNACK_NOT_FOUND_FOR_SDK_VERSION" -> "This Snack is incompatible with this version of Expo Go"
"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED" -> "Project is incompatible with Expo Go"
else -> null
}
} catch (e: JSONException) {
Expand Down Expand Up @@ -116,6 +117,13 @@ class ManifestException : ExponentException {
"You need to log in. If the snack is still unavailable after logging in, ask the owner to grant you access."
}

"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED" -> {
formattedMessage = "This project was published with a precompiled Hermes bytecode bundle. Expo Go runs plain JavaScript bundles, so it can't load this update."
fixInstructions =
"Open this project in a <a href='https://docs.expo.dev/develop/development-builds/introduction/'>development build</a> instead, or use <b>eas update --no-bytecode</b>."
canRetry = false
}

"USER_SNACK_NOT_FOUND", "SNACK_NOT_FOUND" ->
formattedMessage = "No snack found at $manifestUrl."

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import com.facebook.react.ReactHost
import com.facebook.react.bridge.ReactContext.RCTDeviceEventEmitter
import com.facebook.react.devsupport.DefaultDevLoadingViewImplementation
import com.facebook.react.devsupport.DevInternalSettings
import com.facebook.react.devsupport.DevSupportManagerBase
import com.facebook.react.devsupport.DoubleTapReloadRecognizer
import com.facebook.react.devsupport.interfaces.DevSupportManager
import com.facebook.react.interfaces.fabric.ReactSurface
Expand All @@ -34,6 +35,7 @@ import expo.modules.manifests.core.Manifest
import host.exp.exponent.ExponentManifest
import host.exp.exponent.analytics.EXL
import host.exp.exponent.di.NativeModuleDepsProvider
import host.exp.exponent.exceptions.ManifestException
import host.exp.exponent.experience.BaseExperienceActivity.ExperienceContentLoaded
import host.exp.exponent.experience.splashscreen.LoadingView
import host.exp.exponent.factories.ReactHostFactory
Expand All @@ -58,6 +60,7 @@ import org.json.JSONObject
import versioned.host.exp.exponent.ExpoNetworkInterceptor
import versioned.host.exp.exponent.ExponentDevBundleDownloadListener
import versioned.host.exp.exponent.ExponentPackage
import java.io.File
import java.util.LinkedList
import java.util.Queue
import javax.inject.Inject
Expand Down Expand Up @@ -359,7 +362,23 @@ abstract class ReactNativeActivity :
instanceManagerBuilderProperties
)

val devBundleDownloadListener = ExponentDevBundleDownloadListener(progressListener)
var devSupportManager: DevSupportManager? = null
val capturedManifestUrl = manifestUrl
val devBundleDownloadListener = ExponentDevBundleDownloadListener(
progressListener,
downloadedBundleFileProvider = {
(devSupportManager as? DevSupportManagerBase)?.downloadedJSBundleFile?.let(::File)
},
onHermesDetected = {
if (capturedManifestUrl != null) {
val errorJson = JSONObject().apply {
put("errorCode", "EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED")
put("message", "Hermes bytecode bundle is not supported by Expo Go")
}
KernelProvider.instance.handleError(ManifestException(null, capturedManifestUrl, errorJson))
}
}
)

if (delegate.isDebugModeEnabled) {
val debuggerHost = manifest!!.getDebuggerHost()
Expand All @@ -377,6 +396,7 @@ abstract class ReactNativeActivity :
useDevSupport = nativeHost.useDeveloperSupport,
devBundleDownloadListener = devBundleDownloadListener
)
devSupportManager = reactHost.devSupportManager

val bundle = Bundle()
val exponentProps = JSONObject()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright 2015-present 650 Industries. All rights reserved.
package host.exp.exponent.utils

import java.io.File
import java.io.IOException

object HermesBundleUtils {
// https://github.com/facebook/hermes/blob/ae8554141cd3d3f64eb98d70c97112fcc6143d34/include/hermes/BCGen/HBC/BytecodeFileFormat.h#L26-L27
private val HERMES_MAGIC_HEADER = byteArrayOf(
0xc6.toByte(),
0x1f.toByte(),
0xbc.toByte(),
0x03.toByte(),
0xc1.toByte(),
0x03.toByte(),
0x19.toByte(),
0x1f.toByte()
)

fun isHermesBundle(file: File): Boolean {
if (!file.exists() || file.length() < HERMES_MAGIC_HEADER.size) return false
return try {
file.inputStream().use { input ->
val bytes = ByteArray(HERMES_MAGIC_HEADER.size)
val read = input.read(bytes, 0, bytes.size)
read == HERMES_MAGIC_HEADER.size && bytes.contentEquals(HERMES_MAGIC_HEADER)
}
} catch (e: IOException) {
false
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,28 @@ package versioned.host.exp.exponent

import com.facebook.react.devsupport.interfaces.DevBundleDownloadListener
import host.exp.exponent.experience.DevBundleDownloadProgressListener
import host.exp.exponent.utils.HermesBundleUtils
import java.io.File
import kotlin.Exception

/**
* Acts as a bridge between the versioned DevBundleDownloadListener and unversioned
* DevBundleDownloadProgressListener
* DevBundleDownloadProgressListener. Optionally sniffs the downloaded bundle for the
* Hermes magic header and fails instead of succeeding when matched.
*/
class ExponentDevBundleDownloadListener(private val listener: DevBundleDownloadProgressListener) :
DevBundleDownloadListener {
class ExponentDevBundleDownloadListener(
private val listener: DevBundleDownloadProgressListener,
private val downloadedBundleFileProvider: (() -> File?)? = null,
private val onHermesDetected: ((Exception) -> Unit)? = null
) : DevBundleDownloadListener {
override fun onSuccess() {
val bundleFile = downloadedBundleFileProvider?.invoke()
if (bundleFile != null && HermesBundleUtils.isHermesBundle(bundleFile)) {
val error = Exception("hermes bytecode bundle is not supported by expo-go")
onHermesDetected?.invoke(error)
listener.onFailure(error)
return
}
listener.onSuccess()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

NS_ASSUME_NONNULL_BEGIN

extern NSString * const EXRuntimeErrorDomain;
extern NSString * const EXFixInstructionsKey;
extern NSString * const EXShowTryAgainButtonKey;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,10 @@ - (NSError *)formatError:(NSError *)error
formattedMessage = @"The project you requested requires a newer version of Expo Go.";
fixInstructions = @"Download the latest version of Expo Go from the App Store.";
showTryAgainButton = false;
} else if ([errorCode isEqualToString:@"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED"]) {
formattedMessage = @"This project was published with a precompiled Hermes bytecode bundle. Expo Go runs plain JavaScript bundles, so it can't load this update.";
fixInstructions = @"Open this project in a [development build](https://docs.expo.dev/develop/development-builds/introduction/) instead, or use **eas update --no-bytecode**.";
showTryAgainButton = false;
} else if ([errorCode isEqualToString:@"NO_COMPATIBLE_EXPERIENCE_FOUND"]){
formattedMessage = rawMessage; // No compatible experience found at ${originalUrl}. Only ${currentSdkVersions} are supported.
} else if ([errorCode isEqualToString:@"EXPERIENCE_NOT_VIEWABLE"]) {
Expand Down Expand Up @@ -392,6 +396,8 @@ + (NSString * _Nonnull)formatHeader:(NSError * _Nonnull)error {
return @"Project is incompatible with this version of Expo Go";
} else if ([errorCode isEqualToString:@"SNACK_NOT_FOUND_FOR_SDK_VERSION"]) {
return @"This Snack is incompatible with this version of Expo Go";
} else if ([errorCode isEqualToString:@"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED"]) {
return @"Project is incompatible with Expo Go";
}
return nil;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,17 @@ - (void)appLoaderTask:(EXUpdatesAppLoaderTask *)appLoaderTask didFinishWithLaunc
return;
}
_bundle = [NSData dataWithContentsOfURL:launcher.launchAssetUrl];
BOOL isEasUpdate = [_httpManifestUrl.host isEqualToString:@"u.expo.dev"];
if (!isEasUpdate && [EXAppLoaderExpoUpdates isHermesBundle:_bundle]) {
EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:_httpManifestUrl originalUrl:_manifestUrl];
_error = [manifestResource formatError:[NSError errorWithDomain:EXRuntimeErrorDomain code:0 userInfo:@{
@"errorCode": @"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED",
}]];
if (self.delegate) {
[self.delegate appLoader:self didFailWithError:_error];
}
return;
}
if (self.delegate) {
[self.delegate appLoader:self didFinishLoadingManifest:_confirmedManifest bundle:_bundle];
}
Expand Down Expand Up @@ -292,6 +303,16 @@ - (void)appLoaderTaskDidFinishAllLoading:(EXUpdatesAppLoaderTask *)appLoaderTask

#pragma mark - internal

+ (BOOL)isHermesBundle:(NSData *)bundle
{
// https://github.com/facebook/hermes/blob/ae8554141cd3d3f64eb98d70c97112fcc6143d34/include/hermes/BCGen/HBC/BytecodeFileFormat.h#L26-L27
static const uint8_t kHermesMagic[] = { 0xc6, 0x1f, 0xbc, 0x03, 0xc1, 0x03, 0x19, 0x1f };
if (bundle.length < sizeof(kHermesMagic)) {
return NO;
}
return memcmp(bundle.bytes, kHermesMagic, sizeof(kHermesMagic)) == 0;
}

+ (NSURL *)_httpUrlFromManifestUrl:(NSURL *)url
{
NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES];
Expand Down Expand Up @@ -471,6 +492,18 @@ - (void)_loadDevelopmentJavaScriptResource
[self.delegate appLoader:self didLoadBundleWithProgress:progress];
}
} success:^(NSData *bundle) {
if ([EXAppLoaderExpoUpdates isHermesBundle:bundle]) {
EXManifestResource *manifestResource = [[EXManifestResource alloc] initWithManifestUrl:self.httpManifestUrl originalUrl:self.manifestUrl];
NSError *hermesError = [manifestResource formatError:[NSError errorWithDomain:EXRuntimeErrorDomain code:0 userInfo:@{
@"errorCode": @"EXPERIENCE_HERMES_BUNDLE_NOT_SUPPORTED",
}]];
self.error = hermesError;
self.isLoadingDevelopmentJavaScriptResource = NO;
if (self.delegate) {
[self.delegate appLoader:self didFailWithError:hermesError];
}
return;
}
self.isUpToDate = YES;
self.bundle = bundle;
self.isLoadingDevelopmentJavaScriptResource = NO;
Expand Down
Loading
Loading