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
4 changes: 4 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# cmd.exe misreads batch files with bare LF line ends (labels, long lines).
*.cmd text eol=crlf
# Shell scripts run on macOS and Linux only; a CRLF would break the shebang.
*.sh text eol=lf
256 changes: 250 additions & 6 deletions .github/workflows/campaign-windows-hermes.yml
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
name: campaign windows hermes (temporary)

# Beta campaign N-25 / MX-WIN / HOOK-13 (Task 13.7). TEMPORARY: delete after
# the first beta's campaign. Proves (or disproves) a Hermes release build of
# the bare example on a Windows host: hermesc through hermesc-preserve-js.sh,
# the composed map, the debug id in map and bytecode, and the source-map
# upload, which goes to a stub server on the runner. Never a real token, never
# a Bugsee endpoint. The APK is kept as an artifact for the device lane
# (composed-debug-id on the WOD_LX1).
# the first beta's campaign. Proves a Hermes release build on a Windows host,
# for the bare example and for the Expo example after `expo prebuild`:
# hermesc through hermesc-preserve-js.cmd (React Native runs hermesCommand
# with `cmd /c`, which cannot run the .sh), the composed map, the debug id in
# map and bytecode, and the source-map upload, which goes to a stub server on
# the runner. Never a real token, never a Bugsee endpoint. The APKs are kept
# as artifacts for the device lane (composed-debug-id on the WOD_LX1).

on:
workflow_dispatch:
pull_request:
paths:
- .github/workflows/campaign-windows-hermes.yml
- .gitattributes
- packages/react-native/scripts/**
- packages/react-native/plugin/**
- examples/bare/android/**
- examples/expo/**

concurrency:
group: campaign-windows-${{ github.ref }}
Expand Down Expand Up @@ -47,6 +52,11 @@ jobs:
- run: corepack enable
- run: yarn install --immutable

# The wrapper's own tests, on Windows: the .cmd launcher run the way
# React Native runs it (cmd /c), with the repository's hermesc.exe.
- name: Wrapper tests (Windows)
run: yarn jest packages/react-native/scripts/__tests__/hermesc-preserve-js.test.js

# The repo's own stand-in for the symbol API (the upload tests use it):
# POST /apps/<token>/symbols answers with a PUT URL on itself, and the
# PUT's zip is unpacked and its map's debug_id logged.
Expand Down Expand Up @@ -123,3 +133,237 @@ jobs:
${{ runner.temp }}/gradle.log
${{ runner.temp }}/stub.log
if-no-files-found: warn

# The guard for builds wired by an earlier README: a hermesCommand that
# names the .sh fails the bundle task before hermesc, with the fix,
# instead of a NoSuchFileException on the .hbc. hermesCommand is a task
# input, so the bundle task reruns.
- name: A .sh hermesCommand on Windows fails with the fix
shell: pwsh
run: |
Add-Content examples/bare/android/app/build.gradle "`nreact { hermesCommand = file(`"../../node_modules/@bugsee/react-native/scripts/hermesc-preserve-js.sh`").absolutePath }"
Push-Location examples/bare/android
./gradlew.bat :app:createBundleReleaseJsAndAssets -PreactNativeArchitectures=arm64-v8a -PbugseeE2eDebuggable=true *> "$env:RUNNER_TEMP/guard.log"
$code = $LASTEXITCODE
Pop-Location
Get-Content "$env:RUNNER_TEMP/guard.log" | Select-String -Pattern "Bugsee|FAILED|NoSuchFile" | Select-Object -First 20
if ($code -eq 0) { Write-Error "FAIL: the bundle task passed with a .sh hermesCommand"; exit 1 }
if (-not (Select-String -Path "$env:RUNNER_TEMP/guard.log" -SimpleMatch "a shell script, but on Windows" -Quiet)) { Write-Error "FAIL: no Bugsee guard message"; exit 1 }
if (Select-String -Path "$env:RUNNER_TEMP/guard.log" -SimpleMatch "NoSuchFileException" -Quiet) { Write-Error "FAIL: hermesc ran before the guard"; exit 1 }
Write-Output "guard: failed with the fix, before hermesc"
# pwsh would otherwise exit with Gradle's (expected) non-zero code.
exit 0

# The same on the Expo example: the config plugin writes hermesCommand, the
# per-OS launcher, during `expo prebuild` on this Windows runner.
expo-android-release:
name: expo android release (windows-latest)
runs-on: windows-latest
timeout-minutes: 75
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22.18'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- run: corepack enable
- run: yarn install --immutable

# The synthetic token makes the hook upload the map (to the stub);
# uploadSymbols false keeps the Gradle plugin's own uploads off, which is
# not what this job proves.
- name: Expo prebuild (Android) with a synthetic token
working-directory: examples/expo
run: |
cat > app.config.js <<'JS'
module.exports = ({ config }) => ({
...config,
plugins: [['@bugsee/react-native', { appToken: process.env.BUGSEE_TOKEN_ANDROID, uploadSymbols: false }]],
});
JS
CI=1 yarn expo prebuild --clean --no-install --platform android
grep -n 'hermesCommand' android/app/build.gradle
grep -qF '"/scripts/hermesc-preserve-js" + (System.getProperty("os.name").startsWith("Windows") ? ".cmd" : ".sh")' android/app/build.gradle

- name: Release build (Hermes) against the stub symbol API
shell: pwsh
run: |
New-Item -ItemType File -Force "$env:RUNNER_TEMP/stub.log" | Out-Null
$stub = Start-Process node -PassThru -NoNewWindow -ArgumentList @(
'packages/react-native/scripts/__tests__/fixtures/symbol-stub-server.js',
'--port', '8777', '--log', "$env:RUNNER_TEMP/stub.log")
Start-Sleep -Seconds 2
Push-Location examples/expo/android
./gradlew.bat :app:assembleRelease -PreactNativeArchitectures=arm64-v8a --stacktrace *> "$env:RUNNER_TEMP/gradle.log"
$code = $LASTEXITCODE
Pop-Location
Stop-Process -Id $stub.Id -ErrorAction SilentlyContinue
Get-Content "$env:RUNNER_TEMP/gradle.log"
exit $code

- name: Composed map carries a debug id; the bytecode carries the same one
working-directory: examples/expo/android/app/build
run: |
map="$(find generated/sourcemaps -name 'index.android.bundle.map' | head -1)"
echo "composed map: $map"
node -e '
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const id = m.debugId || m.debug_id;
if (!id) { console.error("FAIL: no debugId in the composed map"); process.exit(1); }
console.log("debugId " + id);
require("fs").writeFileSync(process.env.RUNNER_TEMP + "/debug-id", id);
' "$map"
apk=outputs/apk/release/app-release.apk
unzip -o -q "$apk" assets/index.android.bundle -d "$RUNNER_TEMP/apk"
id="$(cat "$RUNNER_TEMP/debug-id")"
if grep -a -q "$id" "$RUNNER_TEMP/apk/assets/index.android.bundle"; then
echo "bytecode carries $id"
else
echo "FAIL: the packaged bytecode does not carry $id"; exit 1
fi

- name: The composed map reached the stub, with that debug id
if: always()
run: |
echo "--- stub requests"; cat "$RUNNER_TEMP/stub.log"
echo "--- hook lines"; grep -i "bugsee" "$RUNNER_TEMP/gradle.log" | head -40 || true
node -e '
const fs = require("fs");
const want = fs.existsSync(process.argv[2]) ? fs.readFileSync(process.argv[2], "utf8").trim() : "";
if (!want) { console.error("FAIL: no composed-map debug id to look for"); process.exit(1); }
const puts = fs.readFileSync(process.argv[1], "utf8").split("\n").filter(Boolean)
.map((l) => JSON.parse(l)).filter((e) => e.method === "PUT");
const hit = puts.some((e) => (e.entries || []).some((x) => x.debugId === want));
if (!hit) { console.error("FAIL: no PUT carried a map with debug_id " + want); process.exit(1); }
console.log("stub received the map with debug_id " + want);
' "$RUNNER_TEMP/stub.log" "$RUNNER_TEMP/debug-id"

- uses: actions/upload-artifact@v4
if: always()
with:
name: campaign-windows-hermes-expo
path: |
examples/expo/android/app/build/outputs/apk/release/app-release.apk
examples/expo/android/app/build/generated/sourcemaps/**
${{ runner.temp }}/gradle.log
${{ runner.temp }}/stub.log
if-no-files-found: warn

# The bare release again, from a copy of the checkout whose path holds
# spaces and parentheses, as C:\Users\First Last\... or "Program Files
# (x86)" do: React Native runs hermesCommand through `cmd /c`, whose quote
# handling changes when the command path has spaces or ( ). The copy sits
# at a short root: under the workspace the New Architecture codegen objects
# pass Windows' 260-character path limit and CMake fails before any
# JavaScript step (run 37600186416, ninja mkdir "No such file or
# directory"), a React Native limit that has nothing to do with spaces.
android-release-spaced-path:
name: android release, path with spaces (windows-latest)
runs-on: windows-latest
timeout-minutes: 75
env:
APP_ROOT: 'C:/s (x86)/b rn'
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Copy the checkout to a path with spaces and parentheses
run: |
mkdir -p "$APP_ROOT"
cp -a . "$APP_ROOT/"
ls "$APP_ROOT"
- uses: actions/setup-node@v4
with:
node-version: '22.18'
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- run: corepack enable
- run: yarn install --immutable
working-directory: 'C:/s (x86)/b rn'

- name: Credentials (synthetic token, stub endpoint, no NDK upload)
working-directory: 'C:/s (x86)/b rn'
run: |
pwd
yarn workspace bugsee-example-bare credentials
sed -i '/^plugin.ndk.enabled/d' examples/bare/android/bugsee.properties

- name: Release build (Hermes) against the stub symbol API
shell: pwsh
run: |
New-Item -ItemType File -Force "$env:RUNNER_TEMP/stub.log" | Out-Null
$stub = Start-Process node -PassThru -NoNewWindow -ArgumentList @(
"`"$env:APP_ROOT/packages/react-native/scripts/__tests__/fixtures/symbol-stub-server.js`"",
'--port', '8777', '--log', "$env:RUNNER_TEMP/stub.log")
Start-Sleep -Seconds 2
Push-Location "$env:APP_ROOT/examples/bare/android"
Write-Output "building in $((Get-Location).Path)"
./gradlew.bat :app:assembleRelease -PreactNativeArchitectures=arm64-v8a -PbugseeE2eDebuggable=true --stacktrace *> "$env:RUNNER_TEMP/gradle.log"
$code = $LASTEXITCODE
Pop-Location
Stop-Process -Id $stub.Id -ErrorAction SilentlyContinue
Get-Content "$env:RUNNER_TEMP/gradle.log"
exit $code

- name: Composed map carries a debug id; the bytecode carries the same one
working-directory: 'C:/s (x86)/b rn/examples/bare/android/app/build'
run: |
map="$(find generated/sourcemaps -name 'index.android.bundle.map' | head -1)"
echo "composed map: $map"
node -e '
const m = JSON.parse(require("fs").readFileSync(process.argv[1], "utf8"));
const id = m.debugId || m.debug_id;
if (!id) { console.error("FAIL: no debugId in the composed map"); process.exit(1); }
console.log("debugId " + id);
require("fs").writeFileSync(process.env.RUNNER_TEMP + "/debug-id", id);
' "$map"
apk=outputs/apk/release/app-release.apk
unzip -o -q "$apk" assets/index.android.bundle -d "$RUNNER_TEMP/apk"
id="$(cat "$RUNNER_TEMP/debug-id")"
if grep -a -q "$id" "$RUNNER_TEMP/apk/assets/index.android.bundle"; then
echo "bytecode carries $id"
else
echo "FAIL: the packaged bytecode does not carry $id"; exit 1
fi

- name: The composed map reached the stub, with that debug id
if: always()
run: |
echo "--- stub requests"; cat "$RUNNER_TEMP/stub.log"
echo "--- hook lines"; grep -i "bugsee" "$RUNNER_TEMP/gradle.log" | head -40 || true
node -e '
const fs = require("fs");
const want = fs.existsSync(process.argv[2]) ? fs.readFileSync(process.argv[2], "utf8").trim() : "";
if (!want) { console.error("FAIL: no composed-map debug id to look for"); process.exit(1); }
const puts = fs.readFileSync(process.argv[1], "utf8").split("\n").filter(Boolean)
.map((l) => JSON.parse(l)).filter((e) => e.method === "PUT");
const hit = puts.some((e) => (e.entries || []).some((x) => x.debugId === want));
if (!hit) { console.error("FAIL: no PUT carried a map with debug_id " + want); process.exit(1); }
console.log("stub received the map with debug_id " + want);
' "$RUNNER_TEMP/stub.log" "$RUNNER_TEMP/debug-id"

- uses: actions/upload-artifact@v4
if: always()
with:
name: campaign-windows-hermes-spaced
path: C:/s (x86)/b rn/examples/bare/android/app/build/outputs/apk/release/app-release.apk
if-no-files-found: warn

# A second artifact: upload-artifact needs one root, and the logs are on D:.
- uses: actions/upload-artifact@v4
if: always()
with:
name: campaign-windows-hermes-spaced-logs
path: |
${{ runner.temp }}/gradle.log
${{ runner.temp }}/stub.log
if-no-files-found: warn
6 changes: 4 additions & 2 deletions examples/bare/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,9 @@ react {

/* Hermes Commands */
// Preserve the JS bundle, then inject the composed map after hermesc.
hermesCommand = file("../../node_modules/@bugsee/react-native/scripts/hermesc-preserve-js.sh").absolutePath
// Windows runs hermesCommand through cmd, which cannot run the .sh.
hermesCommand = file("../../node_modules/@bugsee/react-native/scripts/hermesc-preserve-js" +
(System.getProperty("os.name").startsWith("Windows") ? ".cmd" : ".sh")).absolutePath
//
// The list of flags to pass to the Hermes compiler. By default is "-O", "-output-source-map"
// hermesFlags = ["-O", "-output-source-map"]
Expand Down Expand Up @@ -135,7 +137,7 @@ android {
// createBundle<Variant>JsAndAssets the hook injects the debug id into the
// composed map and the bytecode (Hermes) or Metro's bundle and map (no Hermes),
// then uploads the map when bugsee.properties holds a real token. It fails
// the bundle task if Hermes ran without hermesc-preserve-js.sh (hermesCommand
// the bundle task if Hermes ran without the preserve wrapper (hermesCommand
// above). -PbugseeUploadSourcemaps=false turns the upload off.
apply from: file("../../node_modules/@bugsee/react-native/scripts/bugsee-sourcemaps.gradle")

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"assert:ios-embed": "node scripts/cli-assert-framework-embedded.ts",
"mutate:src": "stryker run stryker.src.json",
"mutate:scripts": "stryker run stryker.scripts.json",
"mutate:plugin": "stryker run stryker.plugin.json; status=$?; chmod 755 packages/react-native/scripts/compose-then-inject.js; exit $status"
"mutate:plugin": "stryker run stryker.plugin.json; status=$?; chmod 755 packages/react-native/scripts/compose-then-inject.js packages/react-native/scripts/hermesc-preserve-js.js; exit $status"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
Expand Down
38 changes: 30 additions & 8 deletions packages/react-native/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -122,17 +122,28 @@ def bugseeDir = new File(["node", "--print", "require.resolve('@bugsee/react-nat

react {
// Copies the JS aside before hermesc compiles it, so the debug id can
// reach the bytecode.
hermesCommand = new File(new File(bugseeDir, "scripts"), "hermesc-preserve-js.sh").absolutePath
// reach the bytecode. React Native runs this command through cmd on
// Windows, so Windows gets the .cmd launcher and macOS and Linux the .sh.
// Keep it on one line: tools that apply this snippet read it as one.
hermesCommand = new File(new File(bugseeDir, "scripts"), System.getProperty("os.name").startsWith("Windows") ? "hermesc-preserve-js.cmd" : "hermesc-preserve-js.sh").absolutePath
}

// At the end of the file. Applying it twice is harmless.
apply from: new File(new File(bugseeDir, "scripts"), "bugsee-sourcemaps.gradle")
```

If Hermes compiles a bundle without going through `hermesc-preserve-js.sh`,
the bundle task fails rather than ship a release without a debug id. With
Hermes off, the plain JS bundle and Metro's map get the id instead.
The OS is checked when Gradle configures the build, so one `build.gradle`
works on macOS, Linux and Windows. Both launchers run
`scripts/hermesc-preserve-js.js` with `node`, which must be on the `PATH` (as
React Native's own bundling already needs). The wrapper exits non-zero when
hermesc writes no bytecode.

If Hermes compiles a bundle without going through the wrapper, the bundle
task fails rather than ship a release without a debug id. On Windows a
`hermesCommand` that names a `.sh` (what earlier versions of this README
said) fails the bundle task before hermesc runs, with the line to use
instead: `cmd` cannot run a shell script. With Hermes off, the plain JS
bundle and Metro's map get the id instead.

- **The upload needs a real token.** Set `app_token` in
`android/bugsee.properties`, or `BUGSEE_APP_TOKEN`. Without a token, or with
Expand All @@ -145,9 +156,20 @@ Hermes off, the plain JS bundle and Metro's map get the id instead.
the path; the Gradle hook takes the path relative to the build directory.
The three agree unless the bundle directory itself nests another
`generated/assets`, which React Native never does.
- **Limitations.** The preserve wrapper is a shell script, so Hermes release
builds on a Windows host are not supported yet. The hook has not yet been
verified with Gradle's configuration cache.
- **Expo.** `expo prebuild` writes the same per-OS choice. A `hermesCommand`
that an earlier prebuild wrote (the `.sh` on every OS) is recognised by its
exact text and moved to the per-OS one; a `hermesCommand` of your own that
already names `hermesc-preserve-js` is left as it is.
- **Windows paths with spaces or brackets.** React Native runs
`hermesCommand` through `cmd /c`, which cuts an absolute path holding a
space together with `(` `)` (`C:\Program Files (x86)\…`), or `(` `)` alone
(`C:\Users\John(US)\…`). On Windows the hook gives the launcher to React
Native relative to the project root instead, as React Native gives its own
paths; the build is verified from `C:\s (x86)\b rn`. A project whose path is long
can still pass Windows' 260-character limit in React Native's own native
build, which has nothing to do with Bugsee.
- **Limitations.** The hook has not yet been verified with Gradle's
configuration cache.

## Licence

Expand Down
2 changes: 2 additions & 0 deletions packages/react-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@
"scripts/hermes-sourcemaps.js",
"scripts/compose-then-inject.js",
"scripts/bugsee-xcode.sh",
"scripts/hermesc-preserve-js.js",
"scripts/hermesc-preserve-js.sh",
"scripts/hermesc-preserve-js.cmd",
"scripts/bugsee-sourcemaps.gradle",
"react-native.config.js",
"*.podspec"
Expand Down
Loading
Loading