Build & Run on Android¶
This guide covers building your ksproject app into an APK (or AAB), signing it, and running it on an emulator or physical device.
Overview¶
The Android build pipeline:
- Pre-build hook — Your
pre_buildscript runs (if configured) - Resolve toolchain — SDK, NDK, Java are downloaded/located automatically
- Install site-packages — Cross-compiled Python packages installed per target architecture
- Collect plugin configs —
.gradle/*.jsonfiles from installed packages are merged - Generate Gradle project — Complete Android project written to
project_dist/gradle/by the bootstrap - Gradle assemble —
./gradlew assembleDebug(orRelease) stages, optimizes, and packages the APK
First Build¶
On first build, ksproject automatically installs everything needed:
This will:
- Download Android command-line tools from Google
- Install SDK platform, build-tools, NDK, CMake via sdkmanager
- Download Java 21 via sdkman (if no compatible JDK found)
- Download CPython for Android (pre-built from the KivySchool index)
- Download SDL2 source (for Java files and native headers)
- Download Gradle wrapper (v9.5.0)
- Cross-compile install your project's Python dependencies for
arm64-v8a - Generate the full Gradle project
- Build the debug APK
First build takes time
The initial build downloads ~2GB of toolchain files. Subsequent builds reuse the cached tools and only rebuild what changed.
Build Commands¶
Debug Build (default)¶
Produces: project_dist/gradle/app/build/outputs/apk/debug/app-debug.apk
Release Build¶
Produces: project_dist/gradle/app/build/outputs/apk/release/app-release-unsigned.apk (unsigned — see Signing below).
Android App Bundle (AAB)¶
For Google Play Store distribution:
Produces: project_dist/gradle/app/build/outputs/bundle/release/app-release.aab
Android Archive (AAR)¶
For library distribution:
Produces an AAR library instead of an APK.
Clean Build¶
Signing for Release¶
Play Store uploads require signed artifacts. ksproject wraps keytool, apksigner, and jarsigner so you never have to locate them yourself.
1. Generate a Keystore (once)¶
uv run ksproject android genkey \
--out my-release-key.jks \
--storepass SecretPass123 \
--keyalias myapp
Optional: --keypass (defaults to the store password) and --dname "CN=My App, O=My Org".
2. Sign the Artifact¶
# Sign the release APK
uv run ksproject android sign \
--keystore my-release-key.jks --storepass SecretPass123 --keyalias myapp
# Sign the release AAB instead
uv run ksproject android sign --bundle \
--keystore my-release-key.jks --storepass SecretPass123 --keyalias myapp
Signing produces app-release-signed.apk / app-release-signed.aab next to the original.
Credentials via .env¶
All flags fall back to environment variables, which ksproject loads from the project's .env file (created by ksproject init, gitignored):
KEYSTORE="my-release-key.jks"
STOREPASS="SecretPass123"
KEYALIAS="myapp"
# KEYPASS defaults to STOREPASS
With .env filled in, signing is just uv run ksproject android sign.
Play Store CI Workflow¶
Writes a tag-triggered GitHub Actions workflow that builds, signs, and uploads an AAB to the Play Store. Configure these repository secrets: ANDROID_KEYSTORE_BASE64, STOREPASS, KEYALIAS, KEYPASS, and PLAY_SERVICE_ACCOUNT_JSON.
Run on Device or Emulator¶
List Available Devices¶
This shows both:
- ADB-connected devices (physical phones/tablets connected via USB or Wi-Fi)
- Available AVDs (Android Virtual Devices / emulators)
Run on a Specific Device¶
# By AVD name — boots the emulator and waits for it
uv run ksproject android run --name "Pixel_8_API_36"
# By adb serial of a device or already-running emulator
uv run ksproject android run --uuid "emulator-5554"
The run command will:
- Find the existing APK (does not rebuild)
- Boot the emulator if you passed
--nameand it isn't running - Install the APK via
adb install - Launch the main activity
Build before running
The run command only installs and launches — it does not build. Always run uv run ksproject android build first if you've made changes.
Specifying Build Variant¶
For the release variant, run prefers a signed APK (app-release-signed.apk), then a standard one, then the unsigned artifact.
Creating an Emulator¶
If you don't have a physical device, create an AVD:
# Get the path to the SDK tools
uv run ksproject android get-path sdk
# Use avdmanager to create a device
$SDK_PATH/cmdline-tools/latest/bin/avdmanager create avd \
--name "Pixel_8_API_36" \
--package "system-images;android-36;google_apis;arm64-v8a" \
--device "pixel_8"
ksproject's SDK installation includes the system image for your configured sdk version and architectures.
Toolchain Paths¶
Query where ksproject installed (or found) the Android tools:
# Android SDK location
uv run ksproject android get-path sdk
# Android NDK location
uv run ksproject android get-path ndk
# Emulator binary location
uv run ksproject android get-path emulator
Generated Project Structure¶
After uv run ksproject android build, the project_dist/gradle/ directory contains a complete Android Studio-compatible project:
project_dist/gradle/
├── app/
│ ├── build.gradle.kts # App module (from your build.tmpl.gradle.kts)
│ └── src/main/
│ ├── AndroidManifest.xml # Generated from AndroidManifest.tmpl.xml + config
│ ├── java/ # Java sources
│ │ ├── org/example/myapp/
│ │ │ ├── MainActivity.java
│ │ │ ├── KivyPythonActivity.java
│ │ │ └── <YourService>.java # one per configured service
│ │ └── org/libsdl/app/ # SDL2 Java (SDLActivity, ...)
│ ├── cpp/ # Native bootstrap
│ │ ├── CMakeLists.txt
│ │ ├── main.c # SDL_main → CPython entry point
│ │ ├── service_main.c # Entry point for background services
│ │ ├── python_include/ # CPython headers per ABI
│ │ └── sdl2_include/ # SDL2 headers
│ ├── jniLibs/ # Native libraries per ABI
│ │ └── arm64-v8a/
│ │ ├── libpython3.so
│ │ └── lib*.so # supporting native libraries
│ ├── assets/
│ │ ├── python3.13/ # Pure-Python standard library
│ │ └── lib-dynload/ # C extension modules per ABI
│ └── res/
│ ├── mipmap/ic_launcher.png
│ └── drawable/ # presplash image (or raw/ for Lottie)
├── site_packages/ # Cross-compiled Python packages per ABI
│ └── arm64-v8a/
├── build.gradle.kts # Root Gradle plugins (+ your gradle_plugins)
├── settings.gradle.kts # Repository config
├── gradle.properties # JVM settings
├── local.properties # SDK path reference
├── gradlew # Gradle wrapper script
└── gradle/
└── wrapper/
└── gradle-wrapper.jar
At build time, Gradle stages site_packages/<abi>, the stdlib, and lib-dynload together, runs the optimization pass, and packs the result into a single compressed assets.zip inside the APK.
Key Build Details¶
| Component | Version | Notes |
|---|---|---|
| Gradle | 9.5.0 | Downloaded automatically |
| Android Gradle Plugin | 8.9.1 | Configured in build.gradle.kts |
| CMake | 3.22.1 | For native code compilation |
| SDL2 | 2.30.11 | Java files + native headers |
| CPython | 3.13 (default) | Pre-built from the KivySchool index; pin via .python-version |
Customizing the Templates¶
Two files in your project root are templates for the generated project — edit them and rebuild:
AndroidManifest.tmpl.xml— the manifest skeleton. Permissions, meta-data, and services from your config are injected into it.build.tmpl.gradle.kts— the app module'sbuild.gradle.ktsskeleton, with{{ placeholder }}slots that ksproject fills in (package name, SDK levels, ABI filters, dependencies...). Add custom Gradle logic here; it survives regeneration, unlike edits to the generated files inproject_dist/.
Size Optimizations¶
Before packaging, an optimization Gradle task runs over the staged Python content:
- Byte-compilation — everything is compiled to
.pyc(optimization level 2) and the.pysources are dropped. Always on for release builds; controlled bybyte_compile_pythonfor debug builds (or force it once with./gradlew -PforceCompile). The compiling interpreter is a uv-managed Python matching the bundled runtime, so.pycmagic numbers always match. - Junk stripping —
tests/,docs/,examples/, caches, and source-only files (.pyi,.c,.h,.pyx,.md,.rst, ...) are removed from the bundled packages. - Symbol stripping — all bundled
.sofiles are stripped with the NDK'sllvm-strip. - Zip packing — the result ships as one compressed
assets.zipinstead of thousands of loose asset files.
Your post_build hook runs on the staged content after these steps, right before AGP packages it.
Multi-Architecture Builds¶
By default, ksproject builds for arm64-v8a only (covers most modern devices). To target multiple architectures:
Each architecture gets:
- Its own cross-compiled site-packages
- Its own
jniLibs/<abi>/directory with native.sofiles - Its own lib-dynload directory in assets
The x86_64 arch is useful for running on Intel-based emulators, but roughly doubles the APK size.
Toolchain Storage¶
Where ksproject stores downloaded tools:
Global tools are shared
New projects are generated with global_tools = true, sharing the SDK/NDK across all your projects (~2GB saved per additional project). Set it to false for a fully self-contained project directory.
Environment Variables¶
These environment variables override ksproject's toolchain resolution:
| Variable | Effect |
|---|---|
ANDROID_HOME / ANDROID_SDK_ROOT |
Use this SDK instead of downloading |
ANDROID_NDK_ROOT |
Use this NDK instead of downloading (ignored if you pin an ndk version) |
JAVA_HOME |
Use this JDK (must be 17–21) |
KIVYSCHOOL_PREBUILT_INDEX |
Custom index URL for pre-built CPython wheels |
KIVYSCHOOL_PREBUILT_BUILD |
Override CPython build number |
KIVYSCHOOL_PREBUILT_API |
Override CPython target API |
KIVYSCHOOL_PREBUILT_DISABLE=1 |
Skip the pre-built CPython and build from source |
KIVYSCHOOL_PREBUILT_FILE_<ARCH> |
Point at a local CPython wheel file (e.g. KIVYSCHOOL_PREBUILT_FILE_ARM64_V8A=/path/to.whl) |
KEYSTORE / STOREPASS / KEYALIAS / KEYPASS |
Signing credentials (usually set in .env) |
Troubleshooting¶
Java Version Issues¶
ksproject requires Java 17–21. Java 22+ causes sdkmanager to crash.
Or let ksproject install Java 21 automatically via sdkman.
SDK License Acceptance¶
On first SDK install, ksproject automatically accepts all Android SDK licenses. No manual intervention needed.
Emulator Won't Start¶
Make sure your system supports hardware acceleration:
- Linux: KVM must be enabled (
/dev/kvmaccessible) - macOS: Hypervisor.framework (Apple Silicon native) or HAXM (Intel)
- Windows: WHPX or HAXM