Repository navigation
Android: Hermes release builds on Windows hosts (Task 13.7, BLK-07) - #65
Conversation
React Native runs react.hermesCommand through `cmd /c` on Windows
(BundleHermesCTask getHermescCommand -> windowsAwareCommandLine), and a
user-set command is kept as is (detectOSAwareHermesCommand). cmd cannot run
hermesc-preserve-js.sh: it exited 0, wrote no bytecode, and the task failed
later with NoSuchFileException on index.android.bundle.hbc.
- The wrapper's work moves to scripts/hermesc-preserve-js.js (Node). Two
launchers start it: hermesc-preserve-js.sh (macOS, Linux; existing builds
keep working) and hermesc-preserve-js.cmd (Windows, CRLF via
.gitattributes). hermesc is found as before, with win64-bin/hermesc.exe
on Windows.
- The wrapper exits non-zero whenever no bytecode came out: hermesc exited
0 with no or an empty -out (a stale file is removed first), was killed,
could not start, or no -out was given.
- hermesCommand picks the launcher when Gradle configures:
System.getProperty("os.name").startsWith("Windows") ? .cmd : .sh, so one
build.gradle serves a repo shared across macOS, Linux and Windows. README,
bare example and the Expo plugin write it.
- Expo plugin: the exact line an earlier prebuild wrote (the .sh on every
OS) is moved to the per-OS one; a user's own hermesCommand that names
hermesc-preserve-js stays. Corpus cases for both.
- bugsee-sourcemaps.gradle: on Windows a Hermes bundle task whose
hermesCommand names a .sh fails before hermesc, with the line to use.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
The windows-latest workflow now also runs the wrapper tests on Windows (the .cmd through cmd /c with the repository's hermesc.exe), checks that a .sh hermesCommand fails the bundle task with the fix before hermesc, and builds the Expo example after an `expo prebuild` on the runner: composed map debug id, the same id in the APK bytecode, and the upload to the stub. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Scoped mutation run (stryker.plugin.json, --mutate on hermesc-preserve-js.js and the hermesCommand rewrite in gradle.ts): 93.86 -> 98.25, the wrapper at 100. The setter-call check before the legacy match could not change the outcome (a setter's value carries its closing bracket), so it goes. The command-line entry runs only in a child process and is excluded from mutation. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
This is a first review of a9ef177 (Windows Hermes release: Node wrapper,
.cmd/.shlaunchers, per-OShermesCommand, Gradle.shguard, Expo migration).The product fix is sound. React Native 0.87
detectOSAwareHermesCommandkeeps a user-set command and only rewrites/to the platform separator — it does not append.exe, so a.cmdlauncher is the right shape. Fail-loud on empty bytecode, the Windows.shdoFirstguard, and the exact-text Expo migration match the BLK-07 failure mode.One real break on this branch: the README groovy snippet is now two lines, and campaign
gen-rn-appstill copies only the first.Did not run Jest (
node_modulesis not present in this environment). CI was still pending at review time.Findings
- P1
packages/react-native/README.md(consumed byscripts/campaign/lib/readme-android.js) — two-linehermesCommandis truncated into invalid Groovy.Verdict
- Overall risk: Medium
- Merge recommendation: Request changes
- Must fix: Restore a single-line README
hermesCommand(or teachreadme-android.jsto take the continuation) and assert the injected line still contains.cmd/.sh.- What works: Node wrapper + thin launchers, stale
-outdeletion, corpus cases for legacy Expo lines,.gitattributesCRLF on.cmd, Windows job covering wrapper tests / the.shguard / Expo prebuild.Sent by Cursor Automation: Bugsee code review
…ndows The windows-latest run showed three test-only failures: a rootless path gains a drive letter when resolved, and the runner's temp directory comes back in 8.3 short form from realpathSync. The launcher tests (the .cmd through cmd /c with hermesc.exe) passed there. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…hole Cursor review (PR 65): scripts/campaign/lib/readme-android.js copies the README's hermesCommand with a one-line match, so the two-line per-OS value was cut after `new File(new File(bugseeDir, "scripts"),` and every generated bare app would fail to configure. The README keeps the value on one line, the shape the Expo plugin writes, and the extractor now refuses a value whose brackets do not close on its line or that does not name both launchers. Test: the real README goes through the extractor whole, and a split value is refused. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of d8d2eda (Windows Hermes release: Node wrapper,
.cmd/.shlaunchers, per-OShermesCommand, Gradle.shguard, Expo migration). Prior finding was on 0b830fb / a9ef177.The previous P1 is fixed. The README
hermesCommandis a single Groovy assignment that names both launchers,readme-android.jsnow refuses a value whose brackets do not close on that line or that omits.cmd/.sh, andscripts/__tests__/campaign-readme-android.test.tsruns the real README through the extractor. Confirmed by executing that same one-line match againstpackages/react-native/README.md(bracket depth 0, both launchers present).No new P0–P2. Checked against surrounding code, not the diff alone:
- React Native 0.87
detectOSAwareHermesCommandkeeps a user-set command and only rewrites/to the platform separator — it does not append.exe, so a.cmdlauncher is the right shape forcmd /c.- The wrapper deletes a stale
-outand exits non-zero when hermesc writes nothing;package.jsonfilesincludes the.js,.sh, and.cmd; the.cmdin tree is CRLF.- The Windows
doFirstguard (endsWith(".sh")) is the intended path for a leftover READMEFile(..., "hermesc-preserve-js.sh")line, which Expo correctly leaves alone (not the exact legacy concat expression).Did not run Jest (
node_modulesis not present). Windows campaign jobs for this HEAD were still queued at review time.Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. The README /
gen-rn-apptruncation is addressed.- What works: One-line per-OS
hermesCommandin the README fence, extractor fail-closed on a split value, Node wrapper + thin launchers, exact-text Expo migration,.gitattributesCRLF on.cmd.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of d8d2eda (Windows Hermes release: Node wrapper,
.cmd/.shlaunchers, per-OShermesCommand, Gradle.shguard, Expo migration).The earlier P1 is fixed: the README
hermesCommandis one closed Groovy line,readme-android.jsrefuses a split value, andcampaign-readme-android.test.tsruns the real README through the extractor.The product path looks right. React Native 0.87
detectOSAwareHermesCommandkeeps a user-set command and only rewrites/to the platform separator — it does not append.exe, so a.cmdlauncher is the correct shape. On windows-latest job 112694086548, 37 of 38 wrapper tests passed, including the.cmdthroughcmd /cwithhermesc.exe. Expo prebuild on the sibling Windows job also succeeded.The remaining break is the one assertion 54df3b4 did not convert to
realpathSync.native. That failure is a hard gate incampaign-windows-hermes.yml, so the bare WindowsassembleRelease(the BLK-07 proof) never ran on this commit.Findings
- P1
packages/react-native/scripts/__tests__/hermesc-preserve-js.test.js—resolveFromstill comparesrealpathSyncpaths; Windows CI dies on 8.3 vs long form and skips the release build.Verdict
- Overall risk: Medium
- Merge recommendation: Request changes
- Must fix: Use
fs.realpathSync.nativeon both sides of theresolveFromassertion (same as thefindHermesctest at line 161). Re-run the Windows wrapper-test job and letassembleReleaseproceed.- What works: One-line README + extractor guard;
.cmdviacmd /c; fail-loud empty bytecode; Windows.shdoFirstguard; Expo exact-text migration of the old.shexpression.Sent by Cursor Automation: Bugsee code review
…ws 8.3) Cursor review (PR 65): the resolveFrom test still used realpathSync, which keeps the runner's 8.3 temp path, while require.resolve returns the long form; it failed on windows-latest before the release build ran. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… seen pwsh ended the step with Gradle's last exit code, the very failure the step expects; run 37593316440 printed 'guard: failed with the fix, before hermesc' and still went red. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of 670c696 (Windows Hermes release: Node wrapper,
.cmd/.shlaunchers, per-OShermesCommand, Gradle.shguard, Expo migration, pwsh guard-step exit). Prior findings were on c4c844b / d8d2eda / 0b830fb.Both earlier P1s are fully_addressed:
- README
hermesCommandis one closed Groovy line that names both launchers;readme-android.jsrefuses a split value. Confirmed by running the same one-line match againstpackages/react-native/README.md(bracket depth 0,.cmdand.shpresent).resolveFromcomparesfs.realpathSync.nativeon both sides. On windows-latest job 112699789721 (c4c844b), wrapper tests,:app:assembleRelease, composed-map debug id, bytecode id, and stub PUT all succeeded.The remaining red on that job was the negative
.shguard step, not the product path. PowerShell keeps Gradle's$LASTEXITCODEafterWrite-Output; 670c696 exits 0 once the three fail-closed checks pass. That matches the log (guard: failed with the fix, before hermescthen a red step).Checked against surrounding code, not the diff alone:
- React Native 0.86.3 and 0.87.1
detectOSAwareHermesCommandkeep a user-set command and only rewrite/toFile.separatorChar. They do not append.exe.windowsAwareCommandLineiscmd /cplus the.cmdpath and hermesc args, which is the shape the launcher tests already run.- The wrapper deletes a stale
-outand exits non-zero when hermesc writes nothing.package.jsonfilesincludes.js,.sh, and.cmd. The.cmdin tree is CRLF (\ronly).- Expo migrates the exact legacy concat expression; a leftover README
File(..., "hermesc-preserve-js.sh")is left as written and is the WindowsdoFirstendsWith(".sh")path.Did not run Jest (
node_modulesis not present). The Windows campaign run for 670c696 was still in progress at review time; the BLK-07 proof already landed on c4c844b.Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. Prior P1s are closed; 670c696 only corrects the guard step's pwsh exit code.
- What works: Node wrapper + thin launchers; one-line README + extractor fail-closed;
.cmdviacmd /cwithhermesc.exeon windows-latest; fail-loud empty bytecode; Windows.shdoFirstguard; Expo exact-text migration of the old.shexpression.Sent by Cursor Automation: Bugsee code review
Same release build, debug-id and stub-upload checks as the bare job, from a checkout under 'my projects (x86)/bugsee rn': React Native runs hermesCommand through cmd /c, whose quote handling changes when the command path holds spaces or ( ). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of edd126f (new
android-release-spaced-pathjob). Prior P1s on d8d2eda / c4c844b remain fully_addressed (one-line READMEhermesCommand+ extractor guard;resolveFromusesrealpathSync.native). Product path on a space-free Windows checkout was already proven on c4c844b (wrapper tests,:app:assembleRelease, composed-map debug id, bytecode id, stub PUT).The new job does not prove
cmd /cquoting. On run 37600186416, job 112722382424 it died in CMake/ninja onCMAKE_OBJECT_PATH_MAXbefore anycreateBundle*/hermesc-preserve-jsran. Yarn install, credentials, and the stub ({"port":8777}) succeeded.Checked against surrounding code, not the diff alone:
- RN 0.87.1
windowsAwareCommandLineiscmd /c+ args;detectOSAwareHermesCommandkeeps a user-set command and only rewrites/→File.separatorChar(nocliPath). Default hermesc (unsethermesCommand) usesFile.cliPath, which is relative on Windows.- This repo still assigns
file(...).absolutePath/getAbsolutePath() + "/scripts/hermesc-preserve-js" + …. That is untested here because CMake failed first. If a short spaced path ever reaches hermesc, that absolute path is the next thing to watch (cmd /ctreats()inside the quoted command as special).- README still says Windows builds are verified without spaces, which matches the red job.
Did not run Jest (
node_modulesis not present). The failure above is from the GitHub Actions log for this HEAD, not from a local Windows run.Findings
- P1
.github/workflows/campaign-windows-hermes.yml— checkoutmy projects (x86)/bugsee rnexceedsCMAKE_OBJECT_PATH_MAX; the job never exercises Hermes/cmd /c.Verdict
- Overall risk: Medium
- Merge recommendation: Request changes
- Must fix: Shorten the spaced checkout (e.g.
p (x)/a) so New Architecture object paths stay under 250 characters, and confirm:app:createBundleReleaseJsAndAssetsactually runs. Drop the job if a short path still cannot host codegen. Do not treat this red X as a Hermes quoting result.- What works: Node wrapper +
.cmd/.shlaunchers on a space-free windows-latest path; one-line README + extractor fail-closed; Windows.shdoFirstguard; Expo exact-text migration of the old.shexpression.Sent by Cursor Automation: Bugsee code review
Under the workspace, 'my projects (x86)/bugsee rn' pushed the New Architecture codegen objects past Windows' 260-character limit, and CMake failed before any JavaScript step. That is React Native's path-length limit, not the spaces; the job now copies the checkout to 'C:/s (x86)/b rn'. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of c258fb3. The P1 from edd126f is fully_addressed: the spaced-path job no longer checkouts under the workspace. It copies to
C:/s (x86)/b rn.The FeedbackSpec object directory that was 228 characters (cap 250) on job 112722382424 becomes 171 with that prefix. The longest New Architecture object path I measured (
ComponentDescriptors.cpp.ounder FeedbackSpec) is 242 ≤ 250. That is the CMake failure from the last push, not a Hermes/cmd /cresult.No new P0–P2. The campaign Windows run for this HEAD was still in progress at review time, so this does not claim
:app:createBundleReleaseJsAndAssetshas succeeded yet — only that the path-length blocker in the workflow is gone.hermesCommandis still.absolutePath(example, README, Expo plugin). RN 0.87 keeps a user-set command as-is and prefixescmd /c; that remains the thing to watch if the short root reaches hermesc and it fails.Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing in the workflow text. Confirm the spaced-path job gets past CMake and actually runs the bundle task.
- What works: Short
C:/s (x86)/b rnroot keeps spaces and(); yarn install runs in the copy; space-free Windows assembleRelease was already green on c4c844b.Sent by Cursor Automation: Bugsee code review
…d ( ) The new windows-latest job from 'C:/s (x86)/b rn' (run 37601601358) failed in createBundleReleaseJsAndAssets with "'C:\s' is not recognized": React Native runs hermesCommand through cmd /c, and cmd strips the quotes around a command path that holds spaces together with ( ) & ^ @ < > |, then runs it cut at the first space. React Native passes its own paths relative to the project root and runs hermesc from there; on Windows the hook now does the same for hermesCommand when its absolute path would be cut (another drive, or a relative path that would be cut too, keeps it as written). README says so. The spaced-path job also splits its artifact, since upload-artifact needs one root. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of d5842db. c258fb3 got past CMake on
C:/s (x86)/b rn, thencreateBundleReleaseJsAndAssetsfailed with'C:\\s' is not recognized(run 37601601358). That is thecmd /cquote rule: spaces plus()in an absolutehermesCommandmake cmd drop the quotes and cut at the first space. RN 0.87windowsAwareCommandLineiscmd /c+ the command;detectOSAwareHermesCommandkeeps a user-set path as-is; default hermesc usesFile.cliPath(relative on Windows) andexec.workingDir(root).This commit does the same for Bugsee: on Windows,
bugsee-sourcemaps.gradlerewrites an absolutereact.hermesCommandthat would be cut to a path relative toreact.root, unless the relative form would still be cut or the files are on different drives. RNTaskConfigurationdoestask.hermesCommand.set(config.hermesCommand)(Property as Provider), so the task sees the rewritten value. The.shdoFirstguard still matches a relative*.sh. Artifact upload is split (APK on C:, logs on D:).No new P0–P2. The campaign Windows run for d5842db was still in progress at review time; this is a code review of the rewrite, not a claim that the spaced-path job is green yet.
Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. Let the spaced-path job confirm
createBundleReleaseJsAndAssetsonC:/s (x86)/b rn.- What works: Relative launcher for
cmd /c; CMake-safe short root; space-free Windows assembleRelease already green on c4c844b; prior README /realpathSync.native/ guard-exit P1s still closed.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of d5842db. Prior P1s remain fully_addressed: one-line README
hermesCommand+ extractor guard;resolveFromusesrealpathSync.native; spaced-path job usesC:/s (x86)/b rnand gets past CMake.The new hook is the right fix for the failure that followed. On run 37601601358, job 112727053095 (
c258fb3),:app:createBundleReleaseJsAndAssetsdied with'C:\s' is not recognized—cmd /cstripped quotes around the absolute.cmdbecause the path holds spaces and(). That is the BLK-07 quoting case, not CMake.Checked against surrounding code, not the diff alone:
- RN 0.81 and 0.87
File.cliPathalready returns a path relative toreact.rooton Windows (workingDiris that root).detectOSAwareHermesCommandkeeps a user-set command as-is (only/→File.separatorChar) and does not callcliPath, which is why.absolutePathbroke.task.hermesCommand.set(config.hermesCommand)is a liveProvider, so rewriting the extension atapply fromtime is what the bundle task runs. Relativize usesnormalize(), soandroid/app/../../node_modules/…becomesnode_modules\…with no spaces or().- Empty default
hermesCommand("") is present but not absolute, so the rewrite no-ops and RN’s own relative autodetect stays.- Another drive, or a relative path that still matches
[\s&<>()@^|], is left as written (documented).Did not run Jest (
node_modulesis not present). Wrapper tests on this HEAD already passed on windows-latest (run 37602820088). The spaced-pathassembleReleasefor d5842db was still in progress at review time; this does not claim that job is green yet — only that the quoting rewrite matches RN’s own path handling and the'C:\s'failure mode.Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. Confirm the spaced-path job’s
:app:createBundleReleaseJsAndAssetssucceeds on this HEAD.- What works: Node wrapper +
.cmd/.shlaunchers; fail-loud empty bytecode; Windows.shdoFirstguard; Expo exact-text migration; one-line README + extractor fail-closed;hermesCommandrelativized fromreact.rootwhencmd /cwould cut the absolute path.Sent by Cursor Automation: Bugsee code review
Run 37602820088 still ran the absolute launcher: the relative form, node_modules\@bugsee\..., was rejected for its @. That character only matters inside cmd's quote rule; an unquoted relative path is cut only by a space or an operator (& < > ^ |), which is now the whole check. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of c34ceba. d5842db relativized
hermesCommandon Windows, then rejected that relative path becausenode_modules\\@bugsee\\...contains@.@is special only inside cmd’s quoted-path rule (&<>()@^|). An unquoted relative command is split by space or& < > ^ |, not by@. The fallback kept the absolute launcher, so run 37602820088 still executedC:\\s (x86)\\...andcmd /ccut it atC:\\s.This commit narrows
bugseeCmdUnsafeto[\\s&<>^|]. The example/README/Expo absolute path still has spaces, so it is still rewritten;node_modules\\@bugsee\\...is now accepted. RN 0.87task.hermesCommand.set(config.hermesCommand)remains a live Provider.No new P0–P2. The campaign Windows run for c34ceba was still queued at review time.
Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. Confirm the spaced-path job’s
createBundleReleaseJsAndAssetson this HEAD.- What works: Relative launcher with a scoped
@bugseesegment; CMake-safeC:/s (x86)/b rnroot; prior P1s (README line,realpathSync.native, MAX_PATH checkout) still closed.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of c34ceba. Prior P1s remain fully_addressed: one-line README
hermesCommand+ extractor guard;resolveFromusesrealpathSync.native; spaced-path job usesC:/s (x86)/b rn.The
@diagnosis from run 37602820088, job 112731270419 (d5842db) is right. Relativize producednode_modules\@bugsee\..., the oldbugseeCmdUnsafe([\s&<>()@^|]) rejected that relative form, and the absolute launcher still died with'C:\\s' is not recognized.@is only special inside cmd's quoted/crule. Java'sProcessImplCMD/WIN32 escape set is space/tab/"<>&|^— it does not quote@— so an unquoted relative@bugseepath is the correct shape. RN 0.81/0.87detectOSAwareHermesCommandstill keeps a user-set command as-is (only/→File.separatorChar) andBundleHermesCTaskstill runs withworkingDir(root).Dropping
@from the shared regex is the right fix for the campaign path (spaces +(x86)). Dropping()along with it is not. The default relative launcher never contains parentheses; putting()back would still acceptnode_modules\@bugsee\...and would still rewriteC:\\Users\\John(US)\\....Findings
- P2
packages/react-native/scripts/bugsee-sourcemaps.gradle— rewrite is skipped when the absolute path has()but no space, soC:\\Users\\John(US)\\...keeps the absolute.cmd.Verdict
- Overall risk: Medium
- Merge recommendation: Request changes
- Must fix: Restore
()in the rewrite/reject set (only@had to leave), or always relativize when the relative form is unquoted-safe ([\s&<>()^|], not@). Add a table test ofbugseeCmdSafeCommandso the next charset edit cannot re-break@bugseeor skip().- What works:
@no longer blocks the relative launcher; Node wrapper +.cmd/.sh; fail-loud empty bytecode; Windows.shdoFirstguard; Expo exact-text migration; one-line README + extractor fail-closed; space-free windows-latestassembleReleasealready green on c4c844b.Sent by Cursor Automation: Bugsee code review
Cursor review (PR 65): c34ceba dropped ( ) from the check, so an absolute launcher path like C:\Users\John(US)\... (no space, so Java does not quote it) was kept, and cmd groups at the bracket. Two checks now: an absolute path holding a space or ( ) & < > @ ^ | is given relative to the project root, and that relative path is used unless it holds a space or ( ) & < > ^ | (@ is fine unquoted). The helper is kept on the project, and the real-Gradle hook test runs it on eight paths: the campaign's spaced path with @, John(US), plain, an operator, another tree, and an already relative path. README names both shapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Review summary
Re-review of 420f41a. The P2 on c34ceba (one regex dropping
(), soC:\\Users\\John(US)\\…never relativized) is fully_addressed. Absolute paths that hold a space or( ) & < > @ ^ |are rewritten; the relative form is used unless it holds a space or( ) & < > ^ |(@allowed unquoted). That is the right split:@is only special inside cmd’s quoted/crule; unquoted( )still group.The real-Gradle hook test now drives
bugseeCmdSafeCommandon the campaign path (space +( )+@bugsee),John(US)with no space, a plain@bugseetree,&, two trees whose relative form would still be cut, and an already-relative path. Author reports run 37604429856 on c34ceba green (debug id in map, bytecode, and stub); this HEAD adds coverage for the no-space parens case that job could not see.No new P0–P2. RN 0.87
task.hermesCommand.set(config.hermesCommand)remains a live Provider. Prior P1s (README line,realpathSync.native, CMAKE_OBJECT_PATH_MAX checkout) remain closed.Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing.
- What works: Relative launcher for
cmd /cincluding scoped@bugseeandJohn(US); CMake-safeC:/s (x86)/b rn; Gradle table for the rewrite; Node wrapper +.cmd/.sh; Windows.shdoFirstguard; Expo exact-text migration; one-line README + extractor fail-closed.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Review summary
Re-review of 420f41a. Prior findings remain fully_addressed: one-line README hermesCommand + extractor guard; resolveFrom uses realpathSync.native; spaced-path job uses C:/s (x86)/b rn.
The previous P2 is fully_addressed. bugseeCmdSafeCommand now has two classes: rewrite an absolute path that holds a space or ( ) & < > @ ^ |, and accept the relative form unless it holds a space or ( ) & < > ^ | (@ stays allowed unquoted). That is the split that c34ceba needed: node_modules\@bugsee\... is kept, and C:\Users\John(US)\... (no space, so Java never quotes it) is rewritten before cmd groups on (US).
Checked against surrounding code, not the diff alone:
- The Gradle helper table covers the campaign path,
John(US), a scoped@bugseepath with no other specials, a path cmd would not cut,&, a relative that would still be cut, and an already-relative path. I walked each case against the two regexes; they match the expected map insourcemaps-gradle.test.js. - RN 0.81/0.87 still keep a user-set
hermesCommand(only/→File.separatorChar) and run it viawindowsAwareCommandLine(cmd /c+ args) withworkingDir(root). Relativizing againstreact.rootis the same rule RN uses for its own paths.task.hermesCommand.set(config.hermesCommand)remains a live Provider, so the rewrite atapply fromtime is what the bundle task runs. - Campaign run 37604429856 on c34ceba already succeeded for space-free Windows
assembleRelease, Expo prebuild+release, and the spaced-path job. 420f41a does not change that relative@bugseeshape; it only extends the absolute trigger to()without a space.
Did not run Jest (node_modules is not present). Confirmed the committed .cmd is CRLF-only and HERMES_COMMAND_EXPR matches between plugin/src/gradle.ts and plugin/build/gradle.js. The Windows campaign for 420f41a was still in progress at review time.
Findings
None remaining.
Verdict
- Overall risk: Low
- Merge recommendation: Approve
- Must fix: Nothing. The
()hole is closed;@bugseeremains unquoted-safe. - What works: Split rewrite/reject sets with a table test; Node wrapper +
.cmd/.shlaunchers; fail-loud empty bytecode; Windows.shdoFirstguard; Expo exact-text migration; one-line README + extractor fail-closed; spaced-pathassembleReleasealready green on c34ceba.
Sent by Cursor Automation: Bugsee code review
…is fixed The job was allowed to fail while the Windows hermesCommand defect was open. #65 fixed it, so drop continue-on-error and describe the job as the blocking N-25 proof it now is. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
) * Android: Hermes release builds on Windows hosts (Task 13.7, BLK-07) React Native runs react.hermesCommand through `cmd /c` on Windows (BundleHermesCTask getHermescCommand -> windowsAwareCommandLine), and a user-set command is kept as is (detectOSAwareHermesCommand). cmd cannot run hermesc-preserve-js.sh: it exited 0, wrote no bytecode, and the task failed later with NoSuchFileException on index.android.bundle.hbc. - The wrapper's work moves to scripts/hermesc-preserve-js.js (Node). Two launchers start it: hermesc-preserve-js.sh (macOS, Linux; existing builds keep working) and hermesc-preserve-js.cmd (Windows, CRLF via .gitattributes). hermesc is found as before, with win64-bin/hermesc.exe on Windows. - The wrapper exits non-zero whenever no bytecode came out: hermesc exited 0 with no or an empty -out (a stale file is removed first), was killed, could not start, or no -out was given. - hermesCommand picks the launcher when Gradle configures: System.getProperty("os.name").startsWith("Windows") ? .cmd : .sh, so one build.gradle serves a repo shared across macOS, Linux and Windows. README, bare example and the Expo plugin write it. - Expo plugin: the exact line an earlier prebuild wrote (the .sh on every OS) is moved to the per-OS one; a user's own hermesCommand that names hermesc-preserve-js stays. Corpus cases for both. - bugsee-sourcemaps.gradle: on Windows a Hermes bundle task whose hermesCommand names a .sh fails before hermesc, with the line to use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: Windows Hermes release job covers Expo and the .sh guard The windows-latest workflow now also runs the wrapper tests on Windows (the .cmd through cmd /c with the repository's hermesc.exe), checks that a .sh hermesCommand fails the bundle task with the fix before hermesc, and builds the Expo example after an `expo prebuild` on the runner: composed map debug id, the same id in the APK bytecode, and the upload to the stub. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper: tests for every helper; drop a redundant setter check Scoped mutation run (stryker.plugin.json, --mutate on hermesc-preserve-js.js and the hermesCommand rewrite in gradle.ts): 93.86 -> 98.25, the wrapper at 100. The setter-call check before the legacy match could not change the outcome (a setter's value carries its closing bracket), so it goes. The command-line entry runs only in a child process and is excluded from mutation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper tests: absolute paths and real paths that hold on Windows The windows-latest run showed three test-only failures: a rootless path gains a drive letter when resolved, and the runner's temp directory comes back in 8.3 short form from realpathSync. The launcher tests (the .cmd through cmd /c with hermesc.exe) passed there. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * README: hermesCommand on one line; the campaign extractor checks it whole Cursor review (PR 65): scripts/campaign/lib/readme-android.js copies the README's hermesCommand with a one-line match, so the two-line per-OS value was cut after `new File(new File(bugseeDir, "scripts"),` and every generated bare app would fail to configure. The README keeps the value on one line, the shape the Expo plugin writes, and the extractor now refuses a value whose brackets do not close on its line or that does not name both launchers. Test: the real README goes through the extractor whole, and a split value is refused. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper tests: resolveFrom compares native real paths (Windows 8.3) Cursor review (PR 65): the resolveFrom test still used realpathSync, which keeps the runner's 8.3 temp path, while require.resolve returns the long form; it failed on windows-latest before the release build ran. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: the Windows guard step exits 0 once the expected failure is seen pwsh ended the step with Gradle's last exit code, the very failure the step expects; run 37593316440 printed 'guard: failed with the fix, before hermesc' and still went red. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: Windows bare release from a path with spaces and parentheses Same release build, debug-id and stub-upload checks as the bare job, from a checkout under 'my projects (x86)/bugsee rn': React Native runs hermesCommand through cmd /c, whose quote handling changes when the command path holds spaces or ( ). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: the spaced-path job builds from a short root Under the workspace, 'my projects (x86)/bugsee rn' pushed the New Architecture codegen objects past Windows' 260-character limit, and CMake failed before any JavaScript step. That is React Native's path-length limit, not the spaces; the job now copies the checkout to 'C:/s (x86)/b rn'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: Windows hermesCommand survives a project path with spaces and ( ) The new windows-latest job from 'C:/s (x86)/b rn' (run 37601601358) failed in createBundleReleaseJsAndAssets with "'C:\s' is not recognized": React Native runs hermesCommand through cmd /c, and cmd strips the quotes around a command path that holds spaces together with ( ) & ^ @ < > |, then runs it cut at the first space. React Native passes its own paths relative to the project root and runs hermesc from there; on Windows the hook now does the same for hermesCommand when its absolute path would be cut (another drive, or a relative path that would be cut too, keeps it as written). README says so. The spaced-path job also splits its artifact, since upload-artifact needs one root. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: the Windows relative hermesCommand may hold @ Run 37602820088 still ran the absolute launcher: the relative form, node_modules\@bugsee\..., was rejected for its @. That character only matters inside cmd's quote rule; an unquoted relative path is cut only by a space or an operator (& < > ^ |), which is now the whole check. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: Windows hermesCommand rewrite also covers ( ) without a space Cursor review (PR 65): c34ceba dropped ( ) from the check, so an absolute launcher path like C:\Users\John(US)\... (no space, so Java does not quote it) was kept, and cmd groups at the bracket. Two checks now: an absolute path holding a space or ( ) & < > @ ^ | is given relative to the project root, and that relative path is used unless it holds a space or ( ) & < > ^ | (@ is fine unquoted). The helper is kept on the project, and the real-Gradle hook test runs it on eight paths: the campaign's spaced path with @, John(US), plain, an operator, another tree, and an already relative path. README names both shapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…is fixed The job was allowed to fail while the Windows hermesCommand defect was open. #65 fixed it, so drop continue-on-error and describe the job as the blocking N-25 proof it now is. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
* campaign: build-lane tooling for the first beta (N-20..N-25, N-27) Generator for one bare app per React Native minor integrated from the packed tarball by the package README (deviations and workarounds logged), Expo SDK 54-57 apps from examples/expo, JSC and package-manager variants with a consumer type-check, the 16 KB alignment check, a temporary windows-latest Hermes release workflow, run-ios.sh delivery/target/app switches (SPM), launch checks and the emulator sweep AVDs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: run-ios.sh restore and test, build-app 16 KB warn, gitignore build-spm 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: commit the generator steps (lib/ was ignored), review fixes - un-ignore scripts/campaign/lib (the root .gitignore's lib/) - Metro ports compose per variant flag, no collisions - run-ios.sh IOS_LAUNCH passes only on BUGSEE_E2E status=2 - launch-check/emulators: CAMPAIGN_DEVICE_LOCK, fail fast on a missing lock dir - check-16kb: APK only; zero 64-bit libraries examined is a FAIL - windows workflow: the repo's symbol stub, PUT with the composed map's debug id asserted, no NDK upload, Gradle's exit code decides - ios-log-mirror: BUGSEE_E2E markers reach NSLog in generated apps 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: build-app runs the embed assertion on simulator builds too 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: launch-check reads single-quoted applicationId (Expo), points simulator Debug at the app's Metro with RCT_jsLocation 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: review round 2 - run-ios.sh IOS_LAUNCH Debug: the app's own Metro (started if needed), -RCT_jsLocation host:port (devicectl after --), 180 s budget - gen-rn-app: W-3 only with workarounds, R-4 only without --readme-only - gen-expo-app: a prebuild without the plugin's Gradle/bundle-phase edits fails - launch-check writes a FAIL row on every early exit; emulators.sh sweep exits non-zero when any launch failed 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: launch-check counts only this launch's status=2; windows job continue-on-error (BLK-07) - launch-check: logcat -c checked, previous dump waited for, 2 s attach, PASS needs status=2 after this launch's own 'launching' line from the same pid - campaign-windows-hermes: continue-on-error with the BLK-07 / Task 13.7 root cause; the job still runs red; the fix branch fix/windows-hermes-release removes the line 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: Hermes release builds on Windows hosts (Task 13.7, BLK-07) (#65) * Android: Hermes release builds on Windows hosts (Task 13.7, BLK-07) React Native runs react.hermesCommand through `cmd /c` on Windows (BundleHermesCTask getHermescCommand -> windowsAwareCommandLine), and a user-set command is kept as is (detectOSAwareHermesCommand). cmd cannot run hermesc-preserve-js.sh: it exited 0, wrote no bytecode, and the task failed later with NoSuchFileException on index.android.bundle.hbc. - The wrapper's work moves to scripts/hermesc-preserve-js.js (Node). Two launchers start it: hermesc-preserve-js.sh (macOS, Linux; existing builds keep working) and hermesc-preserve-js.cmd (Windows, CRLF via .gitattributes). hermesc is found as before, with win64-bin/hermesc.exe on Windows. - The wrapper exits non-zero whenever no bytecode came out: hermesc exited 0 with no or an empty -out (a stale file is removed first), was killed, could not start, or no -out was given. - hermesCommand picks the launcher when Gradle configures: System.getProperty("os.name").startsWith("Windows") ? .cmd : .sh, so one build.gradle serves a repo shared across macOS, Linux and Windows. README, bare example and the Expo plugin write it. - Expo plugin: the exact line an earlier prebuild wrote (the .sh on every OS) is moved to the per-OS one; a user's own hermesCommand that names hermesc-preserve-js stays. Corpus cases for both. - bugsee-sourcemaps.gradle: on Windows a Hermes bundle task whose hermesCommand names a .sh fails before hermesc, with the line to use. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: Windows Hermes release job covers Expo and the .sh guard The windows-latest workflow now also runs the wrapper tests on Windows (the .cmd through cmd /c with the repository's hermesc.exe), checks that a .sh hermesCommand fails the bundle task with the fix before hermesc, and builds the Expo example after an `expo prebuild` on the runner: composed map debug id, the same id in the APK bytecode, and the upload to the stub. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper: tests for every helper; drop a redundant setter check Scoped mutation run (stryker.plugin.json, --mutate on hermesc-preserve-js.js and the hermesCommand rewrite in gradle.ts): 93.86 -> 98.25, the wrapper at 100. The setter-call check before the legacy match could not change the outcome (a setter's value carries its closing bracket), so it goes. The command-line entry runs only in a child process and is excluded from mutation. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper tests: absolute paths and real paths that hold on Windows The windows-latest run showed three test-only failures: a rootless path gains a drive letter when resolved, and the runner's temp directory comes back in 8.3 short form from realpathSync. The launcher tests (the .cmd through cmd /c with hermesc.exe) passed there. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * README: hermesCommand on one line; the campaign extractor checks it whole Cursor review (PR 65): scripts/campaign/lib/readme-android.js copies the README's hermesCommand with a one-line match, so the two-line per-OS value was cut after `new File(new File(bugseeDir, "scripts"),` and every generated bare app would fail to configure. The README keeps the value on one line, the shape the Expo plugin writes, and the extractor now refuses a value whose brackets do not close on its line or that does not name both launchers. Test: the real README goes through the extractor whole, and a split value is refused. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Preserve wrapper tests: resolveFrom compares native real paths (Windows 8.3) Cursor review (PR 65): the resolveFrom test still used realpathSync, which keeps the runner's 8.3 temp path, while require.resolve returns the long form; it failed on windows-latest before the release build ran. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: the Windows guard step exits 0 once the expected failure is seen pwsh ended the step with Gradle's last exit code, the very failure the step expects; run 37593316440 printed 'guard: failed with the fix, before hermesc' and still went red. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: Windows bare release from a path with spaces and parentheses Same release build, debug-id and stub-upload checks as the bare job, from a checkout under 'my projects (x86)/bugsee rn': React Native runs hermesCommand through cmd /c, whose quote handling changes when the command path holds spaces or ( ). 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: the spaced-path job builds from a short root Under the workspace, 'my projects (x86)/bugsee rn' pushed the New Architecture codegen objects past Windows' 260-character limit, and CMake failed before any JavaScript step. That is React Native's path-length limit, not the spaces; the job now copies the checkout to 'C:/s (x86)/b rn'. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: Windows hermesCommand survives a project path with spaces and ( ) The new windows-latest job from 'C:/s (x86)/b rn' (run 37601601358) failed in createBundleReleaseJsAndAssets with "'C:\s' is not recognized": React Native runs hermesCommand through cmd /c, and cmd strips the quotes around a command path that holds spaces together with ( ) & ^ @ < > |, then runs it cut at the first space. React Native passes its own paths relative to the project root and runs hermesc from there; on Windows the hook now does the same for hermesCommand when its absolute path would be cut (another drive, or a relative path that would be cut too, keeps it as written). README says so. The spaced-path job also splits its artifact, since upload-artifact needs one root. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: the Windows relative hermesCommand may hold @ Run 37602820088 still ran the absolute launcher: the relative form, node_modules\@bugsee\..., was rejected for its @. That character only matters inside cmd's quote rule; an unquoted relative path is cut only by a space or an operator (& < > ^ |), which is now the whole check. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * Android: Windows hermesCommand rewrite also covers ( ) without a space Cursor review (PR 65): c34ceba dropped ( ) from the check, so an absolute launcher path like C:\Users\John(US)\... (no space, so Java does not quote it) was kept, and cmd groups at the bracket. Two checks now: an absolute path holding a space or ( ) & < > @ ^ | is given relative to the project root, and that relative path is used unless it holds a space or ( ) & < > ^ | (@ is fine unquoted). The helper is kept on the project, and the real-Gradle hook test runs it on eight paths: the campaign's spaced path with @, John(US), plain, an operator, another tree, and an already relative path. README names both shapes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: the Windows Hermes job is a required proof now that BLK-07 is fixed The job was allowed to fail while the Windows hermesCommand defect was open. #65 fixed it, so drop continue-on-error and describe the job as the blocking N-25 proof it now is. 🤖 Generated with [Claude Code](https://claude.com/claude-code) * campaign: gate the iOS embed check on Node 22.18; bound the emulator boot wait build-app.sh ran the .ts embed check without the Node version gate run-ios.sh has, so an older Node's ERR_UNKNOWN_FILE_EXTENSION (exit 1) was recorded as an embed FAIL. It now checks first and records TOOLING instead. emulators.sh waited for boot without a limit while a sweep held the shared device lock. The wait is bounded (BOOT_TIMEOUT, default 600 s); on timeout the emulator is killed, the sweep records the AVD as FAIL, releases the lock and moves on. 🤖 Generated with [Claude Code](https://claude.com/claude-code)


Fixes release blocker BLK-07 / Task 13.7: a Hermes release build of an app integrated per the README (bare or Expo) could not be built on a Windows host.
Root cause (#61, runs 37566757670, 37567458498, 37584777337, 37587420367):
react.hermesCommandpointed atscripts/hermesc-preserve-js.sh. On Windows, React Native runs that command throughcmd /c(BundleHermesCTask.getHermescCommand→windowsAwareCommandLine;detectOSAwareHermesCommandkeeps a user-set command as is).cmdcan't run a bash script, so the step exited 0 without writing bytecode, and the build failed later withNoSuchFileExceptiononindex.android.bundle.hbc.Design
scripts/hermesc-preserve-js.js(Node). Two thin launchers start it:hermesc-preserve-js.shfor macOS and Linux (builds already wired to it keep working) andhermesc-preserve-js.cmdfor Windows (CRLF line endings, pinned in.gitattributes). hermesc is found the same way as before, usingwin64-bin/hermesc.exeon Windows.hermesCommandpicks the launcher when Gradle configures the build:System.getProperty("os.name").startsWith("Windows") ? ".cmd" : ".sh". Onebuild.gradleworks for a repo shared across macOS, Linux and Windows. I chose this over relying on cmd's extension lookup (PATHEXT) for an extensionless file, because nothing then depends on howcmdresolves names.-out(a stale-outfile is deleted first), when hermesc is killed, when it can't start, or when it gets no-out.bugsee-sourcemaps.gradleguard: on Windows, a Hermes bundle task whosehermesCommandnames a.shfile fails before hermesc runs, and the error gives the line to use. This covers apps wired per the earlier README..shon every OS) and moves it to the per-OS one. A user's ownhermesCommandthat already nameshermesc-preserve-jsis left as it is. A setting that doesn't name the wrapper is rewritten or refused, as before.Tests
scripts/__tests__/hermesc-preserve-js.test.js(new): the script runs in-process for mutation testing, plus the launcher this host uses. On Windows that is the.cmd, run throughcmd /cwith the repository'shermesc.exe..sh, a user.cmd, the old expression with more after it, in.set(…), after;, inside a comment, in another block).hermesCommandnames a.SHis not refused off Windows. The "missing preserve" message now names the per-OS fix.stryker.plugin.jsonnow mutateshermesc-preserve-js.js..shguard, and an Expo job (prebuild on the runner, release build, debug id in the composed map and in the bytecode, upload to the stub).Based on #61, which adds the windows-latest workflow. I'll retarget to
mainonce #61 merges.🤖 Generated with Claude Code