facebook / react-native

A framework for building native applications using React
https://reactnative.dev
MIT License
119.59k stars 24.37k forks source link

Bundling React Native with Hermes manually #34043

Closed marandaneto closed 9 months ago

marandaneto commented 2 years ago

Description

I'd like to generate the bundles of an RN App with Hermes enabled manually, so I could upload them manually to 3rd tools such as Sentry.io in order to symbolicate stack traces.

I've not found official documentation about that so I've tried to do reverse engineering myself.

Linked issue https://github.com/getsentry/sentry-react-native/issues/2244 Sentry's docs: https://docs.sentry.io/platforms/react-native/manual-setup/hermes/

Version

0.67.4

Output of npx react-native info

System: OS: macOS 12.4 CPU: (16) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz Memory: 132.30 MB / 32.00 GB Shell: 5.8.1 - /bin/zsh Binaries: Node: 18.3.0 - /usr/local/bin/node Yarn: 1.22.19 - ~/.yarn/bin/yarn npm: 8.11.0 - /usr/local/bin/npm Watchman: 2022.06.13.00 - /usr/local/bin/watchman Managers: CocoaPods: 1.11.3 - /usr/local/bin/pod SDKs: iOS SDK: Platforms: DriverKit 21.4, iOS 15.5, macOS 12.3, tvOS 15.4, watchOS 8.5 Android SDK: Not Found IDEs: Android Studio: Chipmunk 2021.2.1 Patch 1 Chipmunk 2021.2.1 Patch 1 Xcode: 13.4.1/13F100 - /usr/bin/xcodebuild Languages: Java: 11.0.10 - /Users/user/.sdkman/candidates/java/current/bin/javac npmPackages: @react-native-community/cli: Not Found react: 17.0.2 => 17.0.2 react-native: 0.67.4 => 0.67.4 react-native-macos: Not Found npmGlobalPackages: react-native: Not Found

Steps to reproduce

If I do it on Android:

cd android

./gradlew AssembleRelease
cd..
node_modules/@sentry/cli/bin/sentry-cli releases \
    files <release> \
    upload-sourcemaps \
    --dist <dist> \
    --strip-prefix $projectRoot/fullFolder \
    --bundle index.android.bundle \
    --bundle-sourcemap index.android.bundle.map

Bundle: android/app/build/generated/assets/react/release/index.android.bundle Bundle.map: android/app/build/generated/sourcemaps/react/release/index.android.bundle.map

Everything works as expected, in this case, I'm using the generated bundles by the RN tooling.

If I do it on iOS:

npx react-native bundle --platform ios --dev false --entry-file index.js --reset-cache --bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map --minify false
node_modules/hermes-engine/osx-bin/hermesc -O -emit-binary -output-source-map -out=main.jsbundle.hbc main.jsbundle
node node_modules/react-native/scripts/compose-source-maps.js main.jsbundle.packager.map main.jsbundle.hbc.map -o main.jsbundle.map

In this case, I've generated the bundles myself, now I have to upload it:

sentry-cli releases files $release upload-sourcemaps --dist $dist --strip-prefix $fullRootFolder main.jsbundle main.jsbundle.map

See screenshots in the Snack, code example, screenshot, or link to a repository section.

So my question is:

Am I using the right commands with the right parameters? npx react-native bundle, hermesc and compose-source-maps.js? Am I uploading the right files? Did this behavior change across RN versions? such as 0.63.x - 0.68.x? Is there any official documentation around generating bundles manually with Hermes?

Thanks a bunch.

Snack, code example, screenshot, or link to a repository

How it is:

173807162-9810fda2-8fd5-4a66-b183-95139185bb7e

How it should be:

Screenshot 2022-06-22 at 13 12 23
cortinico commented 2 years ago

Hey hey @marandaneto 👋

Am I using the right commands with the right parameters? npx react-native bundle, hermesc and compose-source-maps.js?

Yes those commands are correct (are the same we're generating during an app build).

Am I uploading the right files?

Yes you are.

Did this behavior change across RN versions? such as 0.63.x - 0.68.x?

Not that I'm aware. It's going to change in 0.69.x though. The significant change is that the path of hermesc will change:

// Before
node_modules/hermes-engine/osx-bin/hermesc
// After
node_modules/react-native/sdks/hermesc/osx-bin/hermesc

Is there any official documentation around generating bundles manually with Hermes?

Sadly the docs on this are scarce as we don't expect users to manually generate sourcemaps. This is what we have on symbolicating JS stacktraces: https://reactnative.dev/docs/symbolication

marandaneto commented 2 years ago

@cortinico Hey hey :)

Awesome, thank you for letting me know the hermes path change, I've added a note on our docs.

So the question is: if we're using the right commands and uploading the files correctly. Why does it work if I upload the generated bundles by the tooling, but it does not if I bundle myself? I can reproduce this issue with a simple template using npx react-native init, Adding Sentry, generating the Bundles, and uploading them to Sentry, so it's not a bug on our end as it works OOTB if I upload the automatically-generated files. Can I provide you with more info to find a solution for this?

Thanks a lot for keeping improving React Native :)

cortinico commented 2 years ago

Can I provide you with more info to find a solution for this?

I would suggest to try to verify that the source map is valid first.

  1. Generate the source map with the commands you're testing
  2. Try to create a crash in your app
  3. Use the metro-symbolicate package to verify that you're able to see the symbolicated stacktrace.

Also is this issue happening only for Hermes?

marandaneto commented 2 years ago

On Android, I've noticed that you have to upload the output (index.android.bundle) of the command 2 instead of command 1, so the .hbc file and then it works.

On iOS, it does not work at all, stack trace and line numbers still don't match.

I've noticed that the output of the command 1 (main.jsbundle) for iOS does not match the auto-generated bundle under /Users/$user/Library/Developer/Xcode/DerivedData/$projectName-$projectId/Build/Products/Release-iphonesimulator/main.jsbundle E.g. using the command 2359677 bytes The auto-generated file 2359629 bytes So either the build isn't deterministic or the commands need to be adjusted for iOS.

Again, if you don't bundle yourself, everything works as expected.

kidroca commented 2 years ago

@marandaneto What seems to be working for me for ios is to skip the hermesc and the compose-source-maps.js steps

So just generate source-maps using

npx react-native bundle --platform ios --dev false --entry-file index.js --reset-cache --bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map --minify false

Then you can check some stack traces locally to verify they are getting decoded

npx metro-symbolicate main.jsbundle.packager.map < ios.stacktrace.txt

Hermes for iOS is enabled (react-native 0.66.4)

I don't know why but combining the packager and the hbc source maps results it metro-symbolicate replacing everything with null

E.g.

npx metro-symbolicate main.jsbundle.map < ios.stacktrace.txt

0  ???                            0x0 shouldComponentUpdate + 199284 (null:null:null)
1  ???                            0x0 checkShouldComponentUpdate + 5581 (null:null:null)
2  ???                            0x0 updateClassComponent + 6842 (null:null:null)

While using just the .package.map file I get

npx metro-symbolicate main.jsbundle.packager.map < ios.stacktrace.txt

0  ???                            0x0 shouldComponentUpdate + 199284 ([redacted]\src\pages\home\report\ReportActionsView.js:114:constructor)
1  ???                            0x0 checkShouldComponentUpdate + 5581 ([redacted]\node_modules\react-native\Libraries\Renderer\implementations\ReactNativeRenderer-prod.js:2527:checkShouldComponentUpdate)
2  ???                            0x0 updateClassComponent + 6842 ([redacted]\node_modules\react-native\Libraries\Renderer\implementations\ReactNativeRenderer-prod.js:4461:updateClassComponent)
3
marandaneto commented 2 years ago

@kidroca awesome, let me try this workaround, thanks.

marandaneto commented 2 years ago

It didn't work for me on iOS, RN 0.67.4, even if uploading the main.jsbundle.packager.map.

kidroca commented 2 years ago

It didn't work for me on iOS, RN 0.67.4, even if uploading the main.jsbundle.packager.map.

From the looks of it the manual setup that uses react-native's own react-native-xcode.sh script uses only the packager map: See this here: https://docs.sentry.io/platforms/react-native/manual-setup/manual-setup/#bundle-react-native-code-and-images

export NODE_BINARY=node
export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"
export SENTRY_PROPERTIES=../sentry.properties

../node_modules/@sentry/cli/bin/sentry-cli react-native xcode \
  ../node_modules/react-native/scripts/react-native-xcode.sh

EXTRA_PACKAGER_ARGS are only for the packager so the output map is only from the packager and not the combined map from the hermes compiler and the packager


To make a combined map automatically through the build phase we need to export SOURCEMAP_FILE

https://github.com/facebook/react-native/blob/4ea38e16bf533955557057656cba5346d2372acd/scripts/react-native-xcode.sh#L125-L128

Which then instructs the script to make 2 maps (if using Hermes) and combine them, but for some reason the combined map doesn't work to decode stack traces, only the packager map works. I guess that's why by default ios build does not generate a source map

Perhaps something goes wrong while the maps are merged because the combined map size is smaller than any of the two maps, I'd expect it'll be as big as the two maps combined

marandaneto commented 2 years ago

If I export SOURCEMAP_FILE=main.jsbundle.map, the map file is generated automatically in the temporary folder, also the bundle. If I upload both files, symbolication still does not work, the same result as the screenshot in the Snack, code example, screenshot, or link to a repository section.

marandaneto commented 2 years ago

So on iOS, we're using the $DERIVED_FILE_DIR env. var to output the main.jsbundle.map file and that goes to /Users/$user/Library/Developer/Xcode/DerivedData/$projectName-$projectId/Build/Intermediates.noindex/$projectName.build/Release-iphonesimulator/$projectName.build/DerivedSources/main.jsbundle.map

If I upload the bundle and this main.jsbundle.map file, it works on iOS, but again, it depends on the automatically generated files and not the manually bundled.

kidroca commented 2 years ago

If I upload the bundle and this main.jsbundle.map file, it works on iOS, but again, it depends on the automatically generated files and not the manually bundled.

What I'm trying to say is because of these packager args, the generated main.jsbundle.map is not the combined map but the packager's map saved as main.jsbundle.map

export EXTRA_PACKAGER_ARGS="--sourcemap-output $DERIVED_FILE_DIR/main.jsbundle.map"

If that works for you, you can generate the same manually Run only the first step here: https://docs.sentry.io/platforms/react-native/manual-setup/hermes/#compile-sourcemaps and ignore the rest

npx react-native bundle --platform ios --dev false --entry-file index.js --reset-cache --bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map --minify false

I'm working on decoding js stacktraces from Firebase Crashlytics and tried this in both existing and a new RN project The js tacktraces I get from Crashlytics can only be decoded by the packager map and not the hbc or the combined map

marandaneto commented 2 years ago

@kidroca Yep, I understood what you meant, still, output is the same, line numbers are all wrong. So just calling npx react-native bundle --platform ios --dev false --entry-file index.js --reset-cache --bundle-output main.jsbundle --sourcemap-output main.jsbundle.packager.map --minify false and uploading both files to Sentry won't help, the problem still lies. If I upload the auto generated files (using EXTRA_PACKAGER_ARGS), it works, so I believe the tooling still does something more than just this command and I have no idea what.

marandaneto commented 2 years ago

Btw what you said, the file main.jsbundle.packager.map matches in size with the one generated by using the EXTRA_PACKAGER_ARGS. The problem is the bundle itself, main.jsbundle differs in size, maybe that's where the problem lies.

kidroca commented 2 years ago

The problem is the bundle itself, main.jsbundle differs in size, maybe that's where the problem lies.

I've noticed that the output of the command 1 (main.jsbundle) for iOS does not match the auto-generated bundle under /Users/$user/Library/Developer/Xcode/DerivedData/$projectName-$projectId/Build/Products/Release-iphonesimulator/main.jsbundle E.g. using the command 2359677 bytes The auto-generated file 2359629 bytes

Can you diff the 2 outputs? For me the difference is just due to the source map comment appended at the end

react-native bundle vs xcode build phase

image

Depending on whether you've touched the build phase there might be no source map comment at all

marandaneto commented 2 years ago

@kidroca that was the trick, this comment is likely used within the symbolication process or within the file itself.

63301c63301
< //# sourceMappingURL=main.jsbundle.packager.map
\ No newline at end of file
---
> //# sourceMappingURL=main.jsbundle.map
\ No newline at end of file

Setting the sourceMappingURL to main.jsbundle.map worked out.

Or just set the file name correctly in command 1 for iOS:

npx react-native bundle --platform ios --dev false --entry-file index.js --reset-cache --bundle-output main.jsbundle --sourcemap-output main.jsbundle.map --minify false

Thanks @kidroca So apparently there's a way out of generating the bundles manually, strange thing is that iOS skips some steps.

kidroca commented 2 years ago

So for you - you had to have both files named main.jsbundle and main.jsbundle.map I guess because you upload both

For me, though, it seems I can name the sourcemap output to anything, this just works:

npx metro-symbolicate my-ios-map-file.map < ios.cralyitics.stacktrace.txt
matt-dalton commented 2 years ago

I'm having an issue even getting the bundle to run on Android. I've created a script to handle this:

if [[ "$OSTYPE" == "linux-gnu"* ]]; then
    OS_BIN="linux64-bin" # osx-bin, win64-bin, or linux64-bin
elif [[ "$OSTYPE" == "darwin"* ]]; then
    OS_BIN="osx-bin" # osx-bin, win64-bin, or linux64-bin
fi

ARTIFACT_FILEPATH="android/app/src/main/assets/"

npx react-native bundle --platform android --dev false --entry-file index.js --reset-cache --bundle-output "$ARTIFACT_FILEPATH"index.android.bundle --sourcemap-output "$ARTIFACT_FILEPATH"index.android.bundle.packager.map --minify false
node_modules/hermes-engine/"$OS_BIN"/hermesc -O -emit-binary -output-source-map -out="$ARTIFACT_FILEPATH"index.android.bundle.hbc "$ARTIFACT_FILEPATH"index.android.bundle
rm -f "$ARTIFACT_FILEPATH"index.android.bundle
mv "$ARTIFACT_FILEPATH"index.android.bundle.hbc "$ARTIFACT_FILEPATH"index.android.bundle
node node_modules/react-native/scripts/compose-source-maps.js "$ARTIFACT_FILEPATH"index.android.bundle.packager.map "$ARTIFACT_FILEPATH"index.android.bundle.hbc.map -o "$ARTIFACT_FILEPATH"index.android.bundle.map

This looks the same to me as what's recommended here (albeit with a different directory). When I try and open up the resulting .apk, I get AndroidRuntime: com.facebook.jni.CppException: Could not get BatchedBridge, make sure your bundle is packaged correctly, so the bundle clearly isn't right.

Has anyone else seen this?

EDIT: I think perhaps I misunderstood here. I think the outputted bytecode bundle is just to be uploaded to sentry, not to actually be used in the .apk. So I need to output this somewhere else.

mym0404 commented 1 year ago

I didn't investigate deeply, but now, Android and iOS with hermes work well without any hermesc or compose source map.

RN: 0.71.2

krystofwoldrich commented 1 year ago

Is it possible that the source maps work without hermesc and compose source map because the React Native tooling doesn'texecute the hermesc and compose source map?

I've created a new app using npx react-native init ... --version 0.70.6 and hermes is enabled by default. The app template shows the "Engine: Hermes" label because global.HermesInternal exists. But the react-native-xcode.sh did not use hermesc and compose source map as the env USE_HERMES is empty.

This was changed in 0.71.2 and by default hermesc is executed. I'm surprised that the source maps work without it.

mym0404 commented 1 year ago

Yes, also, it works well not only in the binary release but also code push release (Android, iOS both and both are using Hermes).

  1. Release code push release with appcenter cli without any config
  2. Bundle manually (Yes it is not ideal because bundling would be built twice).

I bundled assets manually(2) because, appcenter cli is using hermesc, compose source map internally, and it causes a failure with sourcemap flag.

I can't sure why this works, but it shows the correct stack trace in Sentry with code push release.

run: npm install -g appcenter-cli

// 1
appcenter codepush release-react -a {{Your Project}} -d {{Deployment}} -t {{Target Binary Version}} --token {{ Your TOKEN }}

// 2
node node_modules/.bin/react-native bundle --platform ios --dev false --entry-file index.js --bundle-output main.jsbundle --sourcemap-output main.jsbundle.map

node_modules/@sentry/cli/bin/sentry-cli releases files {{ Your Release }} upload-sourcemaps --dist {{ Your dist }} main.jsbundle main.jsbundle.map

image

krystofwoldrich commented 1 year ago

I did some more testing and this is what I've found.

Hermesc -output-source-map has a side effect on the bundle.

If hermes byte code bundle is generated without a source map the packager source map will work.

If hermes byte code bundle is generated with the source map the combined source map will work.

❯ hermesc -O -emit-binary -output-source-map -out=main.jsbundle.hbc.with.source.maps main.jsbundle

❯ ls -lh main.jsbundle.hbc.with.source.maps
-rw-r--r-- 1 krystofwoldrich staff 1.6M Mar 29 10:36 main.jsbundle.hbc.with.source.maps

❯ hermesc -O -emit-binary -out=main.jsbundle.hbc.without.source.maps main.jsbundle

❯ ls -lh main.jsbundle.hbc.without.source.maps
-rw-r--r-- 1 krystofwoldrich staff 2.1M Mar 29 10:36 main.jsbundle.hbc.without.source.maps
mym0404 commented 1 year ago

@krystofwoldrich This seems to be true.

Recently, I bundled manually with sourcrmap and it didn't cause any error with combine sourcemap script(Android). And it works in sentry stacktrace.

In past, I didn't generate sourcemap in bundling process.

github-actions[bot] commented 9 months ago

This issue is stale because it has been open 180 days with no activity. Remove stale label or comment or this will be closed in 7 days.

Mikchail commented 6 months ago

Hello! I'm having same issue with upload manually. Any update?