Yesterday a release build of one of the Android apps in our pnpm monorepo failed after 1 minute 34 seconds. Nothing in that app had changed in a way that could affect the native build. The app I had built just before it, a different app in the same repository, had left something behind, and that was the cause.
This is a short write-up of what the error said, why the fix it suggests is the wrong one, and which four folders actually needed to go.
The error blames your app’s minSdk
The failing task was the CMake configure step, :app:configureCMakeRelWithDebInfo[arm64-v8a], which links native libraries installed by pnpm into the app. The message was:
[CXX1214] .../react-native/ReactAndroid/cmake-utils/default-app-setup/CMakeLists.txt release|arm64-v8a :
User has minSdkVersion 24 but library was built for 26 [//react-native-reanimated/reanimated]
Read literally, it says Reanimated now requires API 26 and the app should raise its minSdkVersion to match. That reading is tempting and wrong. The app had been building at 24 for months, and the Reanimated version (4.3.1) had not changed. Raising the app to 26 would make the build pass, but it would also silently drop Android 7.x devices for no real reason.
The library did not start requiring 26. A copy of it that had been compiled for 26 was still sitting on disk.
pnpm gives eleven apps one copy of the library
The repository is a pnpm workspace with eleven Expo apps. pnpm keeps a single copy of each resolved package version in node_modules/.pnpm and links every workspace project to it. I ran require.resolve('react-native-reanimated/package.json') from each app, and all eleven resolved to the same directory.
That is normally the point of pnpm. It becomes a problem with native libraries that compile C++ as part of the consuming app’s Gradle build. Reanimated writes its intermediates into its own package directory, android/build and android/.cxx. One copy of the package means one copy of the build output, shared by every app.
The apps do not agree on minSdk:
| Apps | minSdk |
|---|---|
| 3 apps (alarm-style apps that set it explicitly) | 26 |
| the other 8, including the one that failed | 24 (Expo default) |
The last build wins
The sequence was simple. I built one of the minSdk 26 apps first, which compiled Reanimated against API 26 and left the result in the shared folder. Then I built the minSdk 24 app. Gradle reused the existing output, the prefab consumer check compared the app’s minSdk with the library’s recorded API level, and refused.
The recorded level lives in a small file inside the library’s prefab package:
android/build/intermediates/prefab_package/release/prefab/modules/reanimated/libs/android.arm64-v8a/abi.json
{ "abi": "arm64-v8a", "api": 24, "ndk": 27, "stl": "c++_shared", "static": false }
When I checked it today it said 24, because the last app built was the minSdk 24 one. After yesterday’s other build it would have said 26.
So the failure depends purely on build order. Build the 24 app and then the 26 app, and both pass, because 26 is allowed to consume a library built for 24. Do it the other way round and the 24 app breaks. The app that breaks is always the one you did not touch, which is exactly why looking for the cause inside it goes nowhere.
The fix is four directories
Delete the build output of Reanimated and its companion Worklets, then build again:
R=$(ls -d node_modules/.pnpm/react-native-reanimated@*/node_modules/react-native-reanimated/android)
W=$(ls -d node_modules/.pnpm/react-native-worklets@*/node_modules/react-native-worklets/android)
rm -rf "$R/build" "$R/.cxx" "$W/build" "$W/.cxx"
All four are regenerated by the next build, so nothing is lost. The rebuild passed, and that build is the one that went to Play review. The directories were larger than I expected, about 2.6 GB together: Reanimated’s build was 1.6 GB and its .cxx 328 MB, Worklets’ were 626 MB and 100 MB.
Wiping node_modules and reinstalling also fixes it. It takes longer and teaches you nothing about which part was stale.
Which other libraries can do this
I counted what else leaves native output inside the shared store. 171 packages under node_modules/.pnpm had an android/build directory. Only four of them publish a prefab package with an abi.json, which is what the minSdk check reads:
- react-native-reanimated
- react-native-worklets
- @shopify/react-native-skia
- react-native-vision-camera
All four currently record "api": 24. I have only seen the failure with Reanimated. I have not reproduced it with Skia or VisionCamera, so I cannot claim they fail the same way, but they use the same packaging and record the same field, so I would expect the same result whenever two apps with different minSdk values share them.
What we changed, and what we did not
Stale build output in a shared location is a recurring theme in this repository. An earlier one was a Turborepo cache hit that restored nothing, where the cache said everything was fine and the output directory was empty. In both cases the tool that reported the problem was correct about what it saw and wrong about where the cause was.
The repository’s agent guide now describes the symptom and the four directories, so the next person who sees this message does not start by raising the app’s minSdk.
The structural fix would be to put every app on minSdk 26. That would remove the mismatch entirely, but it would drop Android 7.x users from the apps that still support them, so I did not do it.
There are things I have not checked. I do not know whether pnpm or Gradle has a setting that keeps native build output per consuming project. I have not tested whether npm or Yarn workspaces with hoisting behave the same way, although the mechanism, a single package directory used by several apps, suggests they would.
Verified on October 6, 2026 with a pnpm workspace, Expo SDK 56, React Native 0.85 and react-native-reanimated 4.3.1.