Skip to content

fix: build RNGH 3 Expo apps for Android on Windows at short roots - #3669

Merged
janicduplessis merged 3 commits into
mainfrom
fix/3641-windows-android-paths
Oct 13, 2026
Merged

janicduplessis merged 3 commits into
mainfrom
fix/3641-windows-android-paths

Conversation

@janicduplessis

@janicduplessis janicduplessis commented Oct 13, 2026 •

Copy link
Copy Markdown
Collaborator

Description

trailhead (Expo SDK 58, RN 0.88, react-native-gesture-handler 3) cannot build for Android on Windows at any realistic root. The NDK's ninja is not long-path aware, and RNGH 3 compiles shared/shadowNodes/.../RNGestureHandlerDetectorShadowNode.cpp into its app-level codegen library. That source is outside the CMake source dir, so CMake names the object after its mangled absolute path. CMake can hash the leading directories only when <object dir>/<md5>/RNGestureHandlerDetectorShadowNode.cpp.o fits CMAKE_OBJECT_PATH_MAX=240, measured from <target>.dir/./. With the shim's stim-<16 hex> staging dir, that needs a root of at most 7 characters for x86_64 (4 for arm64-v8a). Past that, CMake keeps the 330+ character name and ninja fails:

ninja: error: Stat(rngesturehandler_codegen_autolinked_build/CMakeFiles/react_codegen_rngesturehandler_codegen.dir/C_/src/trailhead/node_modules/react-native-gesture-handler/shared/shadowNodes/.../RNGestureHandlerDetectorShadowNode.cpp.o): Filename longer than 260 characters

STIM_PATH_TOO_LONG let those roots through because its tail (198) was measured on safeareacontext, whose object is 27 characters shorter. It was also off by one: CMake's single-config Ninja generator measures the object directory as <target>.dir/./ (source; the base generator's GetCMakeCFGIntDir() is "."), so a shortened path fits only up to 238, and the preflight allowed 239. Its subst X: remedy fails for Expo projects: Node's native realpath, which expo-modules-autolinking uses, resolves the subst drive back to the real path, so Gradle sees two roots (evidence in #3641).

Solution

  • Shorter Windows staging name. On Windows the shim stages CMake under .cxx/<first 4 profile hex chars> instead of .cxx/stim-<16 hex>. This keeps per-profile separation and ccache/build-cache keying as they are, and gives back 17 characters. A 4-hex collision between two profiles of one module would only make them share a CMake directory, which is how builds worked before profiles existed. macOS and Linux keep stim-<16 hex>. Editing the shim changes the profile hash on every platform, so each module configures CMake once more after upgrading.

  • Project-aware preflight, limit 238. androidPathRoom uses the RNGH tail (208) when react-native-gesture-handler/shared/shadowNodes resolves from the project, and the safeareacontext tail (181) otherwise, and refuses a shortened path over 238. A single RNGH-sized constant would refuse the bare e2e fixture's 33-character CI root (and other projects without RNGH 3) even though they build. Max roots:

    x86_64 arm64-v8a armeabi-v7a (all ABIs)
    with RNGH 3 24 21 19
    without 51 48 46

    C:\src\trailhead (16) and the build worker's C:\stimw\<8 hex> (17) fit.

  • Remedy. The error, stim guide errors STIM_PATH_TOO_LONG, doctor and the requirements page now say to move or clone the project under a short real path such as C:\src\<name>, and that a subst drive does not work.

  • doctor --fix's legacy .cxx repair recognizes the 4-hex profile directories as managed, as it already did for stim-<16 hex>.

I considered staging outside the project at a short global path. It would make the root length irrelevant for these objects, but it would need its own uniqueness, ownership and worktree remove/gc cleanup. The in-project rename fixes trailhead at normal C:\src\<name> roots without that machinery.

Measurement. I dumped every object from all 9 CMake modules of a trailhead build on Windows and replayed CMake 3.22's shortening rule (cmLocalGeneratorShortenObjectName, with the ./) over root lengths. For every staging length tried, the RNGH detector object is the binding one. x86_64 max roots per staging name: stim-<16> 7, <6 hex> 22, <4 hex> 24, none 28. Nested node_modules/expo/node_modules/expo-modules-core objects (with the 4-hex staging dir) are hashed, so CMake caps them at 237 characters at a 16-character root, and their hashed form keeps fitting well past the RNGH limit, so they never bind. The hashed detector object came to 230 characters at C:\src\trailhead, which matches 16 + 208 + 6.

Test plan

On a windows-latest runner (NDK 27.1.12297006, CMake 3.22.1, android-34 x86_64 emulator), with trailhead ff910e0 cloned to C:\src\trailhead:

  • Before (main dd09744): stim android fails after 31 min at :app:buildCMakeDebug[x86_64] with the ninja Filename longer than 260 characters error above.
  • After: stim android gives build ok (9m40s), installs and launches com.appandflow.trailhead. Metro served the bundle and the app reported [stim:readiness] ready shortly after the 20 s launch check (which printed UNVERIFIED), and a screenshot shows the trails list.
  • Boundary, after: at C:\src\trailhead-abcdefg (24) with --no-build-cache and fresh .cxx, build ok (4m35s) and launch. The detector object is hashed to exactly 238 characters. At C:\src\trailhead-abcdefgh (25), before the 238 correction, the build failed in ninja with the same Filename longer than 260 characters error (the path came to 239, CMake did not hash it). With the correction, stim android at that root refuses: STIM_PATH_TOO_LONG: ... (react_codegen_rngesturehandler_codegen for x86_64) reaches 239 characters, past the 238 ... the root can be at most 24 characters.
  • Refusal: doctor --platform android at C:\Users\runneradmin\src\trailhead (34) reports the finding with the move-or-clone remedy. (stim android there stopped earlier, at emulator creation, because the runner's C: drive was full.)

pnpm test, test:runtime and test:e2e pass. The unit tests cover the RNGH and safeareacontext boundaries, the shared-shadow-node detection (including a hoisted node_modules), and doctor keeping the 4-hex profile directories.

Fixes #3641

Shorten the Windows CMake profile directory to 4 hex characters, size the STIM_PATH_TOO_LONG preflight for react-native-gesture-handler 3 shared shadow nodes, and replace the subst remedy, which Expo autolinking defeats, with moving the project under a short real path.

Fixes #3641
CMake's single-config Ninja generator measures objects from <target>.dir/./, so a shortened object path fits only up to 238 characters. A trailhead build at a 25-character root failed at 239.
@janicduplessis janicduplessis added area: cli packages/stim-cli, including its README and guide area: website website/ labels Oct 13, 2026
@janicduplessis

Copy link
Copy Markdown
Collaborator Author

Fresh review of e4d6f64 (issue #3641, diff vs origin/main). No blocking findings.

Checked

  • Shim: on Windows, profile.take(4) replaces stim- + profile. macOS and Linux are unchanged. STIM_ANDROID_NATIVE_PROFILE is always set by engine/gradle.ts (16 lowercase hex characters), and the init script only runs through Stim, so a null profile cannot reach take.
  • Limit: I read CMake v3.22.1. ComputeTargetObjectDirectory appends GetCMakeCFGIntDir() + /, which is ./. cmLocalGeneratorShortenObjectName succeeds when <md5>/<file name> fits, so the condition is that the written path is at most 238. That matches the measured 24/25 boundary.
  • Tails, recomputed by hand: safeareacontext is 18+4+7+8+1+1+33+11+34+32+1+31 = 181, and RNGH is 181 + 9 (object file name) + 2×9 (target name) = 208. The guide and requirements numbers match: 24/21/19 and 51/48/46.
  • Non-RNGH projects with nested expo/node_modules/expo-modules-core (58.0.16 sources from jsDelivr): the hashed common/cpp objects (longest is ExpoViewEventEmitter.cpp.o) and the in-source src/main/cpp objects both come to a tail of 176 + ABI under the 4-hex staging. That is below safeareacontext's 181, so the 51-character non-RNGH limit holds for that module.
  • Monorepo: resolvePackageJson walks up from the app root, so hoisted RNGH is detected, and the test covers this. The source location does not change the hashed length, only the staging root does. The win32 early return runs before the filesystem probe, so other hosts pay nothing.
  • Release and all-ABI builds: the existing RelWithDebInfo and armeabi-v7a adjustments still apply to both object tails.
  • doctor-cxx: the regex now also preserves .cxx/<4 hex> directories. That narrows what doctor --fix removes and does not widen it, so it is not a needs-human ownership change.
  • Guides and docs: errors.ts, lifecycle.ts, settings.ts, requirements.md, build-optimizations.md and android-cas.md are consistent with the code. No subst remedy is left outside docs/releases/1.7.0.md, which is historical.
  • Tests: the RNGH boundary (24 builds, 25 refused at 239) and the 33-character fixture root are anchored to real measurements. The detection test exercises real directory layouts, including hoisting. The doctor-cxx test checks the preserved 4-hex directory.
  • Local runs: the android-path-limit, doctor-cxx, engine-gradle and guide tests pass (217), as do typecheck, lint, format:check and knip.

Non-blocking

  1. PR description: "Nested node_modules/expo/node_modules/expo-modules-core objects hash to at most 237 at a 16-character root" can't refer to the 4-hex staging. At the 24-character boundary root it would be 245, and that build succeeded. It reads like a stim-<16 hex> measurement. Consider saying which staging it was measured with. The conclusion ("never bind") holds by the computation above.
  2. androidPathRoom's object option and the longestAndroidObject, GESTURE_HANDLER_OBJECT and SAFE_AREA_CONTEXT_OBJECT exports are used only by tests. I accept this, because the boundary cases need fixed-length roots that don't exist on disk, so I'm not asking for a change.
  3. A 4-hex collision between two profiles of one module shares a CMake directory. The description already covers this, and the odds are 1 in 65,536 per pair.

No needs-human area is touched. Adding review: ai-approved.

@janicduplessis janicduplessis added review: ai-approved A fresh AI review found nothing left to fix e2e-smoke one worktree builds, launches and stops on iOS, Linux Android and Windows Android labels Oct 13, 2026
On Linux CI the temp root is short enough to fit a project without RNGH 3, so the refusal case needs a root longer than 51 characters everywhere.
@janicduplessis janicduplessis removed the review: ai-approved A fresh AI review found nothing left to fix label Oct 13, 2026
@janicduplessis

Copy link
Copy Markdown
Collaborator Author

Fresh review of 795a982 (full diff vs main)

Verdict: clear. No blocking or actionable findings.

Checked

  • android-path-limit.ts: I recomputed both tails from the documented paths (4-hex staging, Debug, 8-char config hash, 32-char md5): safeareacontext = 181, RNGH detector = 208. With the 238 limit (<target>.dir/./), the max roots come out to 51/48/46 without RNGH 3 and 24/21/19 with it, matching the guide, the requirements page and the PR table. longestAndroidObject uses resolvePackageJson, which walks up the tree, so a hoisted monorepo node_modules is covered, and it gates on shared/shadowNodes existing, so RNGH 2 still gets the safeareacontext tail.
  • Shim: the profile.take(4) change is Windows-only, STIM_ANDROID_NATIVE_PROFILE is always set by gradle.ts, and the profile hash covers the shim bytes, so every platform reconfigures once after the upgrade, as the description says. A 4-hex collision only makes two profiles share a staging directory.
  • doctor-cxx.ts: the regex now also preserves lowercase 4-hex dirs at the top of .cxx. That narrows what repair deletes, and AGP's own top-level names (Debug, RelWithDebInfo) are not hex.
  • Temp-dir assumptions: for projects without RNGH 3 the new limit (51) is looser than the old one (35), so the only test that relied on a long tmp root was the buildAndroid refusal test, which 795a982 pads to more than 60 characters. The other win32 buildAndroid test (line ~793) refuses earlier on the SDK check. The doctor win32 tests don't depend on path room, and no fixture installs RNGH shared/shadowNodes.
  • Tests meet the AGENTS.md bar: Windows-measured boundaries (24 builds, 25 refused), project-aware detection including the hoisted case, and doctor preserving the 4-hex dirs.
  • Guides and docs match the code, and the PR description matches the diff.

Ran locally

  • TMPDIR=/tmp pnpm test on android-path-limit, engine-gradle, doctor-cxx, doctor and the guide tests (to match Linux CI's short temp dir): 375 passed. The same files also pass with the default macOS temp dir.
  • pnpm typecheck and pnpm lint (including the type-assertion ratchet) both pass.

Nits (non-blocking)

  • guide/errors.ts and website/docs/requirements.md hard-code 24/21/51/48. They are correct now, but they will go stale if a tail is re-measured.
  • maxRoot in android-path-limit.test.ts derives its expected values from the exported tail constants. The measured-root tests are the independent check, so this is acceptable.

No needs-human category applies: there is no network, secret, ownership-widening, release or breaking on-disk change. Labeled review: ai-approved. Node 22/24 test lanes were still running when I posted this.

@janicduplessis janicduplessis added the review: ai-approved A fresh AI review found nothing left to fix label Oct 13, 2026
@janicduplessis
janicduplessis marked this pull request as ready for review October 13, 2026 21:21
@janicduplessis
janicduplessis added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 42b086f Oct 13, 2026
37 of 45 checks passed
@janicduplessis
janicduplessis deleted the fix/3641-windows-android-paths branch October 13, 2026 21:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: cli packages/stim-cli, including its README and guide area: website website/ e2e-smoke one worktree builds, launches and stops on iOS, Linux Android and Windows Android review: ai-approved A fresh AI review found nothing left to fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows Android builds exceed MAX_PATH for RNGH 3 and nested Expo modules; subst remedy fails for Expo

1 participant