-
Notifications
You must be signed in to change notification settings - Fork 4
Building from source
WM Keyboard is an ordinary Gradle/AGP Android project. There's no bootstrap script to run first, and everything from the dictionary compiler to the two build flavors goes through normal Gradle tasks. This page covers what you need installed, what assembleFullDebug and assembleLiteDebug differ on, and the things that catch people out the first time.
-
Install a JDK. JDK 17 or newer is enough to launch the
gradlewscript. Gradle then runs on a JDK 21 toolchain, which it downloads from Foojay on your first build, pergradle/gradle-daemon-jvm.properties. You do not have to install JDK 21 yourself. -
Install the Android SDK with
compileSdk36.1 available. AGP asks for it asrelease(36) { minorApiLevel = 1 }, so platform 36 on its own is not enough. Android Studio's SDK Manager handles this, or usesdkmanageron the command line.minSdkis 24 (Android 7.0).targetSdkis 36. -
Clone the repo and build.
git clone <repo-url>
cd WMKeyboard
./gradlew assembleFullDebugThe first run downloads the Gradle 9.5 distribution, the JDK 21 toolchain and every Maven dependency. Budget a few minutes and a good connection. Later builds are incremental.
-
Install the APK. Run
adb install app/build/outputs/apk/full/debug/app-full-debug.apk. A default debug build is a single APK restricted toarm64-v8abyndk.abiFilters. ABI-suffixed filenames likeapp-full-arm64-v8a-release.apkonly appear when-Pwmkb.splitApks=trueturns onsplits.abi, which is what the release workflow does. -
Turn the keyboard on. Open the app. 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.
JDK 21 runs Gradle, not your code
The auto-provisioned JDK 21 toolchain is not the project's bytecode target. Every Android module compiles to Java 11 bytecode, with sourceCompatibility, targetCompatibility and Kotlin's jvmTarget all set to 11. The exception is :tools:dictc, a host-only JVM tool that never ships inside the APK and builds on a JDK 17 toolchain. JDK 21 is only what the Gradle daemon itself runs on.
WM Keyboard ships as two build flavors on the same capabilities flavor dimension, declared identically in :app and all 20 :core:*/:feature:* modules. A 21st, :feature:llm, declares the same dimension but only joins the build for Play-channel releases. The standalone :tools:dictc compiler is a plain JVM module and carries no flavors at all.
-
assembleFullDebugbuilds every feature: ML Kit handwriting recognition, ML Kit OCR/QR/document scanning, the Harper grammar checker (and the Harper spell-checker service it registers with Android), the local LLM tool, offline Whisper dictation, and one-tap background removal in the sticker editor. -
assembleLiteDebugswitches all of those off. That drops roughly 100 MB of ML Kit models and the Harper native library from the install, which is what makes it useful on a low-storage device.
Both flavors exist on every module so the project builds uniformly across variants. Four modules actually carry different Kotlin per flavor: core/voice, core/content, core/intelligence and feature/ime each have a real src/full/ and src/lite/ split. :app has a src/full/ as well, holding the spell-checker service's manifest entry and its strings, with nothing on the lite side. Everywhere else the flavor dimension is structural, and the sources compiling under full and lite are identical. core/config mirrors five ENABLE_* flags as BuildConfig booleans so library modules can read them without depending on :app. The sticker cutout has no flag of its own: core/content declares the segmentation library as fullImplementation and its lite stub reports supported = false.
See full vs. lite for what this means from the user's side, and architecture for how the module graph is laid out.
: the grammar checker's native library only ships in full builds.
The grammar tool's linter is a Rust cdylib (native/harper-jni/) that wraps Harper's harper-core behind a small JNI surface. core/intelligence/src/main/java/com/wasimaster/wmkeyboard/core/grammar/HarperNative.kt is the Kotlin entry point. The prebuilt .so files are committed under core/intelligence/src/full/jniLibs/<abi>/, so a normal assembleFullDebug never touches Rust or the NDK. Rebuild the library only if you edit native/harper-jni/src/lib.rs or bump the harper-core version:
# One-time setup
rustup target add aarch64-linux-android armv7-linux-androideabi x86_64-linux-android
cargo install cargo-ndk
brew 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 --releaseThe same command is in native/harper-jni/README.md, along with the JNI contract the Kotlin side depends on.
--platform 24 matches the app's minSdk exactly. Release builds package all three supported ABIs (arm64-v8a, armeabi-v7a and x86_64) through ndk.abiFilters in app/build.gradle.kts. Debug builds restrict to arm64-v8a so local compiles finish sooner. Generic 32-bit x86 is not supported.
./gradlew staticAnalysisThat one task chains every analyzer the project uses: Android Lint (lintFullDebug), plus type-resolved detekt for :app (both flavors and its unit tests) and for nineteen library modules across core/* and feature/*. core/config is skipped because it has no Kotlin sources. tools:dictc is skipped because its build adds :core:prediction's source directory as an extra Kotlin source directory, so analyzing it would report those same files a second time. Detekt also runs a separate lite pass for core/voice, core/intelligence and feature/ime, since a bug in a lite-only stub is invisible to the full-flavor run. core/content has a lite stub too and no lite pass, so add one there if that file grows.
detekt 1.23's Android integration doesn't register detekt tasks under AGP 9. A convention plugin (wmkeyboard.detekt) hand-registers them instead. Each module gets its own detektFullDebug/detektLiteDebug task that analyzes only its own sources against its own compile classpath, which avoids the false positives you get from analyzing one module's code against another's classpath.
Both analyzers read curated configs rather than defaults. config/detekt/detekt.yml builds on detekt's default ruleset, not the more opinionated allRules. 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. Treat a new finding as a real regression.
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 how static analysis fits into a pull request, and testing for running the unit test suite itself.
-
No
local.propertiesis required to build. API keys for the network tools (GIF/sticker search, web search, translation, and the Unsplash/Pexels photo backgrounds) are read fromlocal.properties, falling back to environment variables, falling back to an empty string. The Dropbox and OneDrive backup destinations read their OAuth client ids the same way. A missing key never fails the build. The affected tool just shows a "needs API key" panel at runtime, and a backup destination with no client id is left out of the list instead. -
A missing release keystore doesn't fail a release build, but it doesn't sign it either. If
RELEASE_STORE_FILEisn't set or the file doesn't exist,assembleFullReleasedrops the signing config and emits an unsigned release APK. There's no debug-key fallback, and that's deliberate: a debug-signed "release" looks shippable and isn't, because Play rejects the debug key and nothing installed from such a build could ever be updated by the real one. -
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. -
CI runs on every pull request (
.github/workflows/ci.yml): unit tests (testFullDebugUnitTest), a full and lite debug assemble, an advisory prediction/gesture eval run, and a docs link-check../gradlew staticAnalysisis not in CI, so run it yourself before opening a PR. There's no pull-request or issue template.
- Home
- Getting started
- Typing
- Languages
- Suggestions & correction
- Emoji & expression
-
Tools
- Clipboard manager
- Voice typing
- Offline voice (Whisper)
- Handwriting
- Scanner (OCR, QR, documents)
- Camera tool
- Translate
- Search, Wikipedia & dictionary
- Media controls
- AI chat
- AI tools
- Utility tools
- Snippets & text expansion
- Text editing & cursor tools
- Instruments
- Trackpad
- Calendar
- App launcher
- Learn from text
- Vocabulary
- Resize the keyboard
- The toolbar
- Themes & appearance
- Addons
- Plugins
- Privacy & security
- Accessibility
-
Reference
- Gesture cheat sheet
- Typing
- Hardware shortcuts
- Deep links & launcher shortcuts
- Key press
- File formats
- Dictionaries & words
- Importing from other keyboards
- Languages
- Importing from Espanso
- Appearance
- Keyboard themes
- Troubleshooting
- Glossary
- Keyboard font
- Easter eggs
- Icons
- Layout & size
- Key layouts
- Rows & bars
- Keyboard modes
- Emoji
- Phone number formats
- Tools
- Addons & plugins
- Reference - Accessibility
- Fingerprint lock
- Reference - Data saver
- Reference - Permissions
- Privacy
- Reference - Selection actions
- Servers
- Reference - Backup & restore
- About & diagnostics
- Statistics
- Settings A–Z
- Development