You configured ANDROID_NDK, ANDROID_SDK_ROOT, ANDROID_NATIVE_API_LEVEL 27, ANDROID_STL c++_shared and ANDROID_ABI arm64-v8a exactly as the build guide said, ran Gradle on Windows, and the build died on :opencv:generateJsonModelDebug with an org.gradle.api.tasks.TaskExecutionException. This page is for engineers assembling a slim image-processing library for an Android host that will drive a board-level camera. The task is not broken and neither is your library: generateJsonModel re-runs CMake's configure step with Gradle's own arguments, and somewhere the two toolchain stories disagree. Below you get what the task does, the five values that must agree, the causes ranked by probability, a worked fix for the NDK 21.1.6352462 / API 27 / arm64-v8a / c++_shared case, and the habits that keep the build green.
What generateJsonModel does when it works
When a module declares an externalNativeBuild block with a CMake path, the Android Gradle plugin registers one task per variant: generateJsonModelDebug for the debug variant of the :opencv module, and a release twin. The task runs CMake in a mode that only configures. It probes the compiler, applies the toolchain file from the NDK, and writes a JSON description of the native build — compiler paths, flags, sources — for the plugin's build tasks to consume. No C++ is compiled in this step.
That has one consequence that saves hours: when generateJsonModelDebug fails, CMake's configure failed under Gradle's arguments, before any source file was touched. A configure you ran by hand that succeeded proves nothing, because Gradle re-configures with its own values into its own binary directory.
The five toolchain values that must agree
Five values describe the native toolchain. Each has a CMake-side home and a Gradle-side home, and a generateJsonModel failure is almost always one of these pairs telling different stories — or an old cache holding a third story.
| Value | CMake-side home | Gradle-side home | What a mismatch looks like |
|---|---|---|---|
| NDK version | the per-version ndk folder under the SDK, here ndk\21.1.6352462 | ndkVersion in build.gradle (legacy: ndk.dir in local.properties) | the cache points at one NDK folder, Gradle injects another |
| ABI | ANDROID_ABI | abiFilters under defaultConfig > externalNativeBuild > cmake | the cache was built for arm64-v8a, Gradle also asks for armeabi-v7a |
| STL | ANDROID_STL | arguments "-DANDROID_STL=c++_shared" | c++_static in the cache, c++_shared from Gradle |
| API level | ANDROID_NATIVE_API_LEVEL, applied as ANDROID_PLATFORM | minSdkVersion, which the plugin turns into an android-N platform | the cache was configured at android-27, Gradle injects android-24 |
| CMake binary | cmake.dir (legacy) | version under externalNativeBuild > cmake | the plugin asks for 3.10.2.4988404 and that folder is absent |
Keep one home per value: build.gradle owns the versions, and local.properties keeps machine paths like sdk.dir only.
Likely causes, most probable first
- A stale CMake cache in the build tree. You or an earlier script ran CMake by hand, or a toolchain value changed without deleting the old output, and the cache still holds the first story. Test: search the module's build output for CMakeCache.txt and read its ANDROID_NDK, ANDROID_ABI and ANDROID_STL lines. Any line that disagrees with build.gradle is your failure. Fix: delete the build output and rebuild (steps below).
- Two homes for the NDK. ndk.dir and cmake.dir in local.properties from a manual setup, plus ndkVersion or a plugin-resolved NDK. Test: list every place a version string appears — local.properties, build.gradle, environment variables — and keep exactly one.
- STL disagreement between modules. The OpenCV module's arguments say c++_shared while the app module defaults to c++_static, or the reverse. Test: compare the externalNativeBuild blocks of every module that compiles native code.
- API level below the configured one. minSdkVersion sits below the level the cache was configured with, so Gradle injects android-24 while the cache says android-27. Test: compare minSdkVersion with the API level your CMake invocation used.
- A missing or wrong CMake binary. The version folder the plugin asks for does not exist under the SDK's cmake directory, or a desktop CMake answered instead. Test: confirm the folder cmake\3.10.2.4988404 exists under the SDK.
The first cause covers most cases, and it costs one delete to test.
Worked example: :opencv:generateJsonModelDebug with NDK 21.1.6352462 and API level 27
The inputs
The failing project runs on Windows and asks for exactly this toolchain:
- NDK 21.1.6352462, installed under the SDK as ndk\21.1.6352462. The SDK's default location on Windows is %LOCALAPPDATA%\Android\Sdk; one NDK install runs roughly 2–3 GB on disk, and two of them is the first sign of a duplicate story.
- API level 27, requested through ANDROID_NATIVE_API_LEVEL.
- ABI arm64-v8a, the 64-bit ARM ABI; the configure log confirms the target when it prints "Detected processor: aarch64".
- STL c++_shared.
- CMake 3.10.2.4988404, the version folder under the SDK's cmake directory.
- Scope: image processing only, no video modules, with settings.gradle trimmed so only the :opencv module builds.
The rule
Each of the five values lives in exactly one place: build.gradle. Nothing configures the toolchain by hand, and the CMake binary directory starts empty, so the cache can only contain the values Gradle passes.
The check, value by value
With the block below in place, each pair reads equal before the task runs:
- ndkVersion "21.1.6352462" = the SDK folder ndk\21.1.6352462 = the ANDROID_NDK the fresh cache records. One string, three appearances, one declaration.
- abiFilters "arm64-v8a" = ANDROID_ABI=arm64-v8a in the cache = the ABI of the device (aarch64, 64-bit ARM).
- minSdkVersion 27 = android-27, because the plugin turns minSdkVersion into the ANDROID_PLATFORM value = the ANDROID_NATIVE_API_LEVEL 27 that used to be passed by hand. One number, one home.
- arguments "-DANDROID_STL=c++_shared" = ANDROID_STL=c++_shared in the cache = the libc++_shared.so that must ship inside lib/arm64-v8a/ of the APK.
- version "3.10.2.4988404" = the SDK folder cmake\3.10.2.4988404.
The block
android {
ndkVersion "21.1.6352462"
defaultConfig {
minSdkVersion 27
externalNativeBuild {
cmake {
arguments "-DANDROID_STL=c++_shared", "-DBUILD_LIST=core,imgproc"
abiFilters "arm64-v8a"
}
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
version "3.10.2.4988404"
}
}
}
BUILD_LIST is OpenCV's module whitelist: core,imgproc builds the image-processing core and drops video, videoio and the rest, which is the scope the project asked for.
The decision
If these five declarations are the only homes for those values and the build output is deleted first, generateJsonModelDebug has nothing left to disagree with. The configure either succeeds, or it fails with a real CMakeLists error — a missing tool, a missing path — that reads as such in the log. Toolchain fights end here; content errors begin.
Fix it step by step
- Declare one NDK: ndkVersion "21.1.6352462" in the module's android block. Delete ndk.dir and cmake.dir from local.properties and keep sdk.dir. The file stays machine-local and out of version control.
- Declare one API level: minSdkVersion 27. Stop passing ANDROID_NATIVE_API_LEVEL by hand anywhere.
- Declare one ABI and one STL under defaultConfig > externalNativeBuild > cmake: arguments "-DANDROID_STL=c++_shared" and abiFilters "arm64-v8a". Add "-DBUILD_LIST=core,imgproc" to hold the library at the image-processing scope.
- Pin the CMake: version "3.10.2.4988404" next to path "CMakeLists.txt" under externalNativeBuild, and confirm cmake\3.10.2.4988404 exists under the SDK.
- Delete the module's build output directory, and any .cxx folder Gradle keeps beside it. Expect to free a few hundred MB; this is the step that removes the stale CMakeCache.txt carrying the old story.
- Run gradlew :opencv:generateJsonModelDebug. The task should now finish with no TaskExecutionException, and the configure log should match the next section.
- Build and install, then open the APK and confirm lib/arm64-v8a/ holds your library plus libc++_shared.so. With c++_shared that file must ship, or the app fails to load the library on the device.
What a healthy configure log looks like
- "Check for working CXX compiler: %LOCALAPPDATA%\Android\Sdk\ndk\21.1.6352462\toolchains\llvm\prebuilt\windows-x86_64\bin\clang++.exe", reported as working and then skipped. The NDK folder in that path must be the one you declared.
- "Detected processor: aarch64" — the ABI argument took, and the target really is 64-bit ARM.
- A PythonInterp pair: "Could NOT find PythonInterp (missing: PYTHON_EXECUTABLE)" followed by "Found PythonInterp" and a Python 3.9 path. OpenCV's configure probes for an old interpreter first, fails, then finds a newer one. For a native build without Python bindings this pair is noise, not an error; it costs a second of configure time.
If the compiler path names a different ndk folder than you declared, a second source of truth survived. Go back to step 1.
When a clean rebuild is not the answer
Then the failure is not a toolchain fight, and the error text picks the next move:
- UnsupportedClassVersionError in the stack: the Java toolchain, not the native one. The build ran Gradle or a plugin on a JDK older than the class files need, and the fix lives in the JDK and Gradle versions; the same signature is worked through in the UnsupportedClassVersionError Gradle build fix page.
- CMake Error naming a missing file or command inside CMakeLists.txt: content, not versions. If this is the first build after a library upgrade, the camera SDK build errors after a library upgrade checklist lists what changed.
- Relocation or PIC complaints at link time: the CMake -fPIC relocation errors when linking a vision SDK article covers that family.
- The app installs but will not load the library: packaging, not building. The Linux vision shared library errors guide runs the same reasoning — find which .so the loader asked for and who was supposed to provide it.
- The project still follows the pre-Gradle OpenCV-on-Android manager layout: the OpenCV Android Gradle SDK migration guide maps that setup onto build.gradle and externalNativeBuild.
- Two modules compile native code and only one fails: align their externalNativeBuild blocks, or drop the second. The trimmed settings.gradle the project already adopted is the cleaner route.
Keep it green
- One home per value: versions in build.gradle; local.properties keeps sdk.dir and nothing else.
- Changed a toolchain value? Delete the build output before the next run. The up-to-date check trusts the cache you are trying to invalidate.
- Trim scope with the module whitelist argument, and commit the trimmed settings.gradle so every machine builds the same module set.
- Record the winning set — NDK 21.1.6352462, API 27, arm64-v8a, c++_shared, CMake 3.10.2.4988404 — in the repository README, next to the Gradle and plugin versions.
The one-source-of-truth habit reaches the Java side of the same Android project too, as the Eclipse Milo configuration for Android Gradle builds walkthrough shows for an industrial protocol library.
For neighbouring failure modes — container builds, ABI mismatches, version pinning — the systems topic page collects the guides.
From a green build to a running camera
The library exists to read frames on the device, so the next decisions are hardware ones. The board-level cameras category holds the compact modules built into such hosts; the camera finder filters them by sensor format, mount and interface, and the configurator starts a full build from the camera step. For choosing the module itself, the board-level camera selection guide and the board camera evaluation checklist cover sensor, mount and interface; when the first frames arrive, capturing images without saving files shows how to pull them off the device without filling its storage.
Questions
Can I keep ndk.dir and cmake.dir in local.properties?
Keep sdk.dir; drop the other two. Recent plugin releases resolve the NDK from ndkVersion (or ndkPath) in build.gradle, and an ndk.dir folder name left over from a manual setup is exactly the second story that makes generateJsonModel fail. local.properties is machine-local and should stay out of version control; the versions belong in build.gradle, where every machine that checks the project out gets the same ones.
Why does CMake say 'Could NOT find PythonInterp' if the build is healthy?
OpenCV's configure probes for a Python 2.7 interpreter first, reports it missing, then finds a newer Python 3 on the machine — the pair of lines in the log is that probe, not a failure. Compiling the native library without Python bindings needs neither interpreter; the search exists for the Python wrapper targets. Configure the build with the Python wrapper options off for a cleaner log and a slightly faster configure.
Which STL should ship with an arm64-v8a vision library?
c++_shared keeps a single libc++_shared.so for the whole process and is what the worked build sets; it must be packaged in lib/arm64-v8a/ or the library fails to load on the device. c++_static bakes a private STL copy into every .so, which breaks when two native libraries in one process both use the STL. Pick one, declare it once, and never mix it across modules in the same app.
Does generateJsonModelDebug run on every build?
No. It is a configuration step, and the plugin re-runs it when its inputs change — CMakeLists.txt, the externalNativeBuild values, or the toolchain itself. The trap is that an old cache can survive a value change and make the next configure fail; that is why every toolchain change here ends with deleting the build output rather than trusting the up-to-date check.
What does 'Detected processor: aarch64' confirm?
That CMake is configuring for 64-bit ARM, which is what arm64-v8a means: the ABI argument took and the compiler being probed is the NDK's aarch64 clang. If your target device is 32-bit ARM, x86 or an emulator, this line is the early warning that the wrong ABI reached CMake — fix abiFilters before the build wastes time compiling for a target you cannot deploy to.