Skip to content

fix: Detect newArchEnabled at subproject scope on Android#1314

Open
jmalmo wants to merge 1 commit into
mrousavy:mainfrom
jmalmo:fix/android-newarch-subproject-scope
Open

fix: Detect newArchEnabled at subproject scope on Android#1314
jmalmo wants to merge 1 commit into
mrousavy:mainfrom
jmalmo:fix/android-newarch-subproject-scope

Conversation

@jmalmo
Copy link
Copy Markdown

@jmalmo jmalmo commented May 4, 2026

What I changed

I switched isNewArchitectureEnabled() from rootProject.hasProperty(...) to project.hasProperty(...) in the four bundled android/build.gradle files (react-native-nitro-modules, the Nitrogen packages/template, and both test packages), plus the example app's helper.

I also dropped newArchEnabled=true from example/android/gradle.properties to pin the regression — build-android.yml now exercises the same code path real consumers hit.

Why

I hit this on a fresh React Native 0.83 / Expo SDK 55 dev-client app with react-native-nitro-modules and react-native-mmkv@^4 installed. bun android failed at :app:configureCMakeDebug[arm64-v8a]:

CMake Error at .../Android-autolinking.cmake:
  add_subdirectory given source
  ".../node_modules/react-native-nitro-modules/android/build/generated/source/codegen/jni/"
  which is not an existing directory.

After digging in: on RN >= 0.82 the new architecture is mandatory and newArchEnabled is no longer required to live in gradle.properties. React Native's ReactRootProjectPlugin instead sets the property on each subproject's extraProperties (see ReactRootProjectPlugin.kt — the subprojects { ... } block sets extraProperties.newArchEnabled = "true" per subproject; the root project itself is not touched). The current rootProject-scoped check misses that path — the bundled libs skip apply plugin: "com.facebook.react", no codegen tasks register, and the consumer build dies during CMake configure.

I think this is the same root cause as #1031 — the reporter there worked around it by regenerating android/ from a fresh template (which re-introduced newArchEnabled=true in gradle.properties and masked the gate), so the issue got closed before the underlying bug was pinned. It's also reproducible after a stale, non---clean Expo prebuild, where the existing gradle.properties stays put and never picks up the new template's content.

project.hasProperty(...) covers all three cases without dropping legacy-arch support:

  • RN >= 0.82 — finds the property via ReactRootProjectPlugin's subproject extraProperties.
  • Legacy RN with explicit opt-ingradle.properties values propagate to every project, so project.hasProperty(...) still returns true.
  • Legacy RN without opt-in — neither scope carries the property, the gate stays closed, and the src/oldarch source path is preserved.

I considered going the more aggressive route and deleting the gate entirely (mirroring callstack/react-native-pager-view#1047), but Nitro's peerDependencies are still permissive (react-native: "*") and src/oldarch is non-empty, so the conservative fix felt right. Happy to switch if you'd prefer the harder line.

Scope — what this PR does not fix

To be upfront about this: my reproduction scenario in the "Why" section above mentions both react-native-nitro-modules and react-native-mmkv@^4. This PR only fixes nitro itself. It does not fix consumer apps that already depend on a published version of react-native-mmkv (or any other Nitrogen-bootstrapped library), because:

  • The fix to packages/template/android/build.gradle only affects libraries that bootstrap from the template after this PR lands. Existing libraries carry a copy of the gate baked into their own published build.gradle at the time they were created, not regenerated by Nitrogen on each build.
  • react-native-mmkv@4.x's packages/react-native-mmkv/android/build.gradle (line 18 on main) still reads rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true".
  • For the user-reported scenario (RN 0.83 + nitro + mmkv) to actually build green without a withGradleProperties config-plugin workaround, react-native-mmkv and any other downstream Nitrogen-bootstrapped library need their own follow-up PRs that mirror this change. I'll send those separately and will link them here.

The Nitrogen generator itself (createGradleExtension.ts) does not emit the gate — verified by inspection — so once this PR lands, newly-bootstrapped Nitro modules will pick up the fix from packages/template.

Test plan

I removed newArchEnabled=true from example/android/gradle.properties, so the existing build-android.yml workflow now fails on main (the bundled libs skip codegen) and passes with this PR. I verified locally with the same Gradle invocation CI uses:

cd example/android
./gradlew :app:assembleDebug -PreactNativeArchitectures=x86_64

Result: BUILD SUCCESSFUL in 2m 34s. generateCodegenArtifactsFromSchema and configureCMakeDebug[x86_64] ran for react-native-nitro-modules, react-native-nitro-test, react-native-nitro-test-external and the app, and :app:assembleDebug completed.

The Gradle build cache (actions/cache in build-android.yml) cannot mask the regression: cache key hashes **/*.gradle* (which this PR modifies), and per-library build/generated/source/codegen/jni/ outputs aren't in the cache key, so a stale cache hit can't paper over a missing codegen task.

Other checks I ran:

  • bun install (postinstall builds nitro, nitrogen and both test packages) — green.
  • bun typecheck — green.
  • bun lint — green.
  • bun specs not run; no nitrogen output changes.

Notes

  • Targets main. No nitrogen spec changes, no spec/test removals, no unrelated edits.
  • References [0.82] Android Build Failed #1031 (same root cause; closed without the underlying bug being pinned).

`isNewArchitectureEnabled()` in react-native-nitro-modules, the Nitrogen
module template and both internal test packages reads
`rootProject.hasProperty("newArchEnabled")`. On RN >= 0.82 with Expo SDK
55, the new architecture is mandatory and the property is no longer
required to live in `android/gradle.properties`; React Native's
`ReactRootProjectPlugin` instead sets `newArchEnabled=true` on each
subproject's extraProperties. The rootProject-scoped check therefore
returns false in that scenario, the libraries silently skip
`apply plugin: "com.facebook.react"`, no codegen tasks register, and the
consumer app fails at `:app:configureCMakeDebug` with:

  CMake Error at .../Android-autolinking.cmake:
    add_subdirectory given source
    ".../node_modules/react-native-nitro-modules/android/build/generated/source/codegen/jni/"
    which is not an existing directory.

This was previously reported in mrousavy#1031 (closed without identifying the
root cause).

Switch the helper to `project.hasProperty("newArchEnabled")`. That
covers both the RN >= 0.82 path (subproject extraProperties) and the
legacy explicit-opt-in path (`gradle.properties` values propagate to
every project), and keeps the `src/oldarch` source path viable for
RN < 0.82.

Pin the regression by removing `newArchEnabled=true` from
`example/android/gradle.properties`. Without the helper change the
example app fails to build under `build-android.yml`; with it the
build is green.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 4, 2026

@jmalmo is attempting to deploy a commit to the Margelo Team on Vercel.

A member of the Team first needs to authorize it.

@jmalmo
Copy link
Copy Markdown
Author

jmalmo commented May 4, 2026

Follow-up filed: mrousavy/react-native-mmkv#1037 — same one-line fix applied to mmkv's independent copy of the gate. As called out in the Scope section above, mmkv's regression-pin test is currently blocked on this PR landing + a tagged nitro release; the mmkv PR is therefore scoped to just the helper change for now.

@mrousavy
Copy link
Copy Markdown
Owner

mrousavy commented May 5, 2026

Not sure if I fully get this - we still use rootProject for stuff like excluded ABI architectures (x86, arm) - so why do we need project here instead of rootProject? Isn't the new architecture flag set only once on the rootProject, and project is the library here itself? You cannot set new arch to true on one project ("library/app") but not on the other.

@jmalmo
Copy link
Copy Markdown
Author

jmalmo commented May 5, 2026

The asymmetry is in RN's gradle plugin. reactNativeArchitectures is in gradle.properties and cascades, so root and subprojects both see it — that's why root works for it.

newArchEnabled is different on RN >= 0.82. It's no longer expected in gradle.propertiesReactRootProjectPlugin injects it at runtime, only on subprojects:

class ReactRootProjectPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    checkLegacyArchProperty(project)
    project.subprojects { subproject ->
      ...
      // We set the New Architecture properties to true for all subprojects. So that
      // libraries don't need to be modified and can keep on using the isNewArchEnabled()
      // function to check if property is set.
      ...
      subproject.extraProperties.set(PropertyUtils.NEW_ARCH_ENABLED, "true")
      subproject.extraProperties.set(PropertyUtils.SCOPED_NEW_ARCH_ENABLED, "true")
    }
  }
}

(PropertyUtils.NEW_ARCH_ENABLED == "newArchEnabled" — see PropertyUtils.kt.)

Nothing in there sets the property on the root. So from inside :react-native-nitro-modules:

  • rootProject.hasProperty("newArchEnabled") → root's own properties → not set → false
  • project.hasProperty("newArchEnabled") → this subproject's properties → set on lines 40–41 above → true

I verified this directly by adding a debug println to the helper on this PR's branch and running ./gradlew help in example/android with newArchEnabled=true removed from gradle.properties:

[VERIFY] :react-native-nitro-modules rootProject.hasProperty('newArchEnabled')=false project.hasProperty('newArchEnabled')=true

Which is why the current gate

return rootProject.hasProperty("newArchEnabled") && rootProject.getProperty("newArchEnabled") == "true"

skips apply plugin: "com.facebook.react" on RN >= 0.82 setups whose gradle.properties has dropped the legacy property — RNGP isn't applied to the library, the react { ... } extension and its codegen tasks aren't registered, the build/generated/source/codegen/jni/ directory is never created, and the consumer app's autolinking CMake fails on the missing add_subdirectory(...). RN's own comment in the plugin (lines 31–33) makes the design intent explicit: "libraries don't need to be modified and can keep on using the isNewArchEnabled() function to check if property is set" — but that only holds if isNewArchEnabled() reads from the scope where the plugin actually writes.

To your "you cannot set new arch to true on one project but not the other" point — agreed, and the fix isn't doing that. Lines 40–41 unconditionally set both keys to "true" on every subproject; the fix is just about reading from the scope where RN actually writes.

There's an alternative I considered and rejected: callstack/react-native-pager-view#1047 deletes the gate entirely on the basis that 0.82+ is new-arch-only. Defensible if you'd rather drop src/oldarch and pin the peer-dep floor — happy to switch this PR over if that's the direction you'd prefer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants