Building from source
Clone, build, install: JDK, SDK and flavor details.
WM Keyboard is a standard Gradle/AGP Android project: there’s no special bootstrap script, and everything from the dictionary compiler to the two build flavors runs through ordinary Gradle tasks. This page covers what you need installed, what assembleFullDebug and assembleLiteDebug actually differ on, and the handful of things that trip people up the first time.
Setting up your toolchain
Section titled “Setting up your toolchain”-
Install a JDK. You need JDK 17 or newer just to launch the
gradlewscript. Gradle itself then runs on a JDK 21 toolchain that it downloads automatically via Foojay the first time you build, pergradle/gradle-daemon-jvm.properties. You don’t need to install JDK 21 yourself. -
Install the Android SDK with
compileSdk36 available (Android Studio’s SDK Manager handles this, orsdkmanageron the command line).minSdkis 24 (Android 7.0);targetSdkis 36. -
Clone the repo and build.
Terminal window git clone <repo-url>cd WMKeyboard./gradlew assembleFullDebugThe first run downloads the Gradle 9.5 distribution, the JDK 21 toolchain, and every Maven dependency, so budget a few minutes and a good connection. Later builds are incremental.
-
Install it.
adb install app/build/outputs/apk/full/debug/app-full-arm64-v8a-debug.apk: thearm64-v8asegment comes from the ABI split (splits.abiinapp/build.gradle.kts), not from the version name. Then open the app and follow the setup card to enable WM Keyboard as a system input method. See installation and initial setup for the in-app side of this.
Building full vs. lite
Section titled “Building full vs. lite”WM Keyboard ships as two build flavors on the same capabilities flavor dimension, declared identically in :app and all 19 :core:*/:feature:* modules (the standalone :tools:dictc compiler is a plain JVM module and doesn’t carry flavors at all):
assembleFullDebug— every feature: ML Kit handwriting recognition, ML Kit OCR/QR/document scanning, the Harper grammar checker, the local LLM tool, and offline Whisper dictation.assembleLiteDebug— all five of those switched off, which drops roughly 100 MB of ML Kit models and the Harper native library from the install. Useful for low-storage devices.
Both flavors exist on every module so the project builds uniformly across variants, but only three modules actually carry different source code per flavor: core/voice, core/intelligence, and feature/ime each have a real src/full/ and src/lite/ split. Everywhere else the flavor dimension is structural only; the sources compiling under full and lite are identical. core/config mirrors the same five ENABLE_* flags as BuildConfig booleans so library modules can read them without depending on :app.
See full vs. lite for what this means from the user’s side, and architecture for how the module graph is laid out.
Rebuilding the native grammar engine
Section titled “Rebuilding the native grammar engine”Full edition only: the grammar checker’s native library only ships in full builds.
The grammar tool’s linter is a Rust cdylib (native/harper-jni/) wrapping Harper’s harper-core behind a small JNI surface, with core/intelligence/src/main/java/com/wasimaster/wmkeyboard/core/grammar/HarperNative.kt as the Kotlin entry point. The prebuilt .so files are committed to the repo under core/intelligence/src/full/jniLibs/<abi>/, so a normal assembleFullDebug never touches Rust or the NDK at all. You only need to rebuild this if you’re editing native/harper-jni/src/lib.rs itself or bumping the harper-core version:
# One-time setuprustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-androidcargo install cargo-ndkbrew install --cask android-ndk # or any NDK; set ANDROID_NDK_HOME
# Build all three ABIs, from native/harper-jni/ANDROID_NDK_HOME=/opt/homebrew/share/android-ndk \cargo ndk -t arm64-v8a -t armeabi-v7a -t x86_64 --platform 24 \ -o ../../core/intelligence/src/full/jniLibs build --release--platform 24 matches the app’s minSdk exactly. Release builds package all three supported ABIs (arm64-v8a, armeabi-v7a, and x86_64) via ndk.abiFilters in app/build.gradle.kts, while debug builds restrict to arm64-v8a for faster local compilation. Generic 32-bit x86 is not supported.
Running static analysis
Section titled “Running static analysis”./gradlew staticAnalysisThis one task chains every analyzer the project uses: Android Lint (lintFullDebug), and type-resolved detekt for :app (both flavors, plus its unit tests) and for eighteen library modules across core/* and feature/*. core/config is skipped because it has no Kotlin sources, and tools:dictc is skipped because its source set is symlinked from :app and would just report the same files twice. Detekt additionally runs a separate lite pass for core/voice, core/intelligence, and feature/ime (the three modules with real full/lite source divergence), since a bug in a lite-only stub is invisible to the full-flavor run.
detekt 1.23’s Android integration doesn’t register detekt<Variant> tasks under AGP 9, so these are hand-registered by a convention plugin (wmkeyboard.detekt) rather than coming from the detekt Gradle plugin directly: each module gets its own detektFullDebug/detektLiteDebug task analyzing only its own sources against its own compile classpath, which avoids the false positives you’d get analyzing one module’s code against another’s classpath.
Both analyzers read from curated configs rather than defaults: config/detekt/detekt.yml builds on detekt’s default ruleset (not the more opinionated allRules), and config/lint/lint.xml starts from every Lint check enabled (including the roughly 200 that are off by default), then silences the ones that don’t apply to an IME, each with a written reason, and escalates crash-class issues to build-breaking errors. There’s no baseline file anywhere in the repo, so staticAnalysis is expected to run clean; a new finding is a real regression, not baseline noise to suppress.
If you want the Kotlin compiler itself to fail the build on any warning (unused results, redundant casts, and the like), pass -PwarningsAsErrors=true:
./gradlew assemble -PwarningsAsErrors=trueThis is off by default so a mid-refactor warning doesn’t block a local build.
See contributing for when static analysis is required before a PR merges, and testing for running the unit test suite itself.
Details & edge cases
Section titled “Details & edge cases”- No
local.propertiesis required to build. API keys for the network tools (GIF/sticker search, web search, translation) are read fromlocal.properties, falling back to environment variables, falling back to an empty string: a missing key never fails the build, the affected tool just shows a “needs API key” panel at runtime. - A missing release keystore doesn’t fail a release build either. If
RELEASE_STORE_FILEisn’t set or the file doesn’t exist,assembleFullReleasesilently signs with the debug keyconfig instead of failing, so you always get an installable APK even without release signing set up. - Module build files can’t declare their own repositories.
settings.gradle.ktssetsrepositoriesMode = FAIL_ON_PROJECT_REPOS, so only thegoogle()/mavenCentral()repositories declared centrally are allowed: adding arepositories { }block inside any module’sbuild.gradle.ktshard-fails the build rather than silently working. - Dictionaries compile automatically. Every build variant wires in a
compileBundledDictionariestask that runs the:tools:dictccompiler overdictionaries-src/*.txtand produces the.wmdictassets bundled into the APK. There’s no separate manual compilation step. See the dictionary pipeline for the format and how to add or edit a wordlist. - There’s no CI pipeline or contribution template in this repo yet.
./gradlew staticAnalysisand./gradlew testFullDebugUnitTestare the same checks a human runs locally, so running them yourself before opening a PR is the closest thing to a gate that exists today.
