Kivy School Project Manager — A cross-platform build toolchain for Python apps targeting Android (iOS/macOS, windows and linux soon).
ksproject generates a pure AGP (Android Gradle Plugin) Gradle project, downloads the Android SDK, NDK, and JDK completely on demand, and builds your APK, AAB, or AAR.
No buildozer, no python-for-android at build time — just a clean, native Gradle project under project_dist/gradle/ that you can open and debug directly in Android Studio.
- Python 3.13
- uv (used for environment and dependency management)
Everything else (Android SDK, NDK, JDK, Gradle wrapper) is downloaded automatically on the first build into .kivyschool/ inside your project directory or globally.
uv init --package hello-world --python 3.13
cd hello-world
uv add git+https://github.com/kivy-school/ksproject@master --dev
uv run ksproject init
ksproject init handles the heavy lifting of project scaffolding. It:
- Writes a starter
main.pyandapp.kvinsrc/hello_world/. - Creates a
src/hello_world/services/directory (with an__init__.py) for Android background services. - Adds a comprehensive
[tool.kivy-school]block to yourpyproject.toml. - Generates
AndroidManifest.tmpl.xmlandbuild.tmpl.gradle.ktsin your project root so you can customize native build behaviors directly. - Creates a
.javafolder for native code injections.
uv add kivy
pyjnius is only meaningful on Android. Mark it with a PEP 508 marker so uv won't try to install it on macOS, Linux, or Windows:
Note
These are already present by default!!
uv add "pyjnius ; sys_platform == 'android'"
Same trick for iOS (coming soon):
uv add "pyobjus ; sys_platform == 'ios'"
Optional scientific stack:
uv add numpy matplotlib
uv run hello-world
This launches the entry point declared in pyproject.toml — the exact same code that will run on Android, with no changes needed.
uv run ksproject android build
The first run downloads the required toolchain:
- Android command-line tools + SDK (API
36, build-tools) - NDK
28c(or your configured version) - Temurin JDK 21 (via
sdkman) - Gradle 9.5.0 wrapper
- CPython-for-Android prebuilt wheels
The output APK lands at:
project_dist/gradle/app/build/outputs/apk/debug/app-debug.apk
# Build a Release APK
uv run ksproject android build release
# Build an Android App Bundle (.aab) for Google Play
uv run ksproject android build release --bundle
# Build an AAR (library) instead of an app
uv run ksproject android build release --aar
List attached devices and available AVDs:
uv run ksproject android devices
Example output:
avd name=Pixel9
device serial=emulator-5554 state=device model=sdk_gphone64_arm64
Boot an AVD by name (boots it, builds the app, installs it, and launches):
uv run ksproject android run --name Pixel9
Plug a phone in over USB with USB debugging enabled (Settings → Developer options → USB debugging). Authorise the host when the dialog pops up.
Find its adb serial:
uv run ksproject android devices
# device serial=R3CN30XXXXX state=device model=SM-G998B
Run by serial:
uv run ksproject android run --uuid R3CN30XXXXX
(Append --variant release to run the production build instead of debug).
ksproject includes built-in tools for generating secure keystores and signing your production artifacts (.apk and .aab) using standard Android SDK/JDK tools.
To keep your passwords out of your terminal history and to make commands significantly shorter, save your credentials in a .env file at the root of your project. ksproject will automatically detect them. Create a .env file and add your variables like this:
# .env
# App Signing Credentials
KEYSTORE="my-release-key.jks"
KEYALIAS="myapp"
STOREPASS="SecretPass123"
#KEYPASS="defaults_to_storepass"This is required only when your key is outdated or you need to generate a new key for the first time.
Using explicit flags:
uv run ksproject android genkey --out my-release-key.jks --storepass SecretPass123 --keyalias myappUsing your .env file:
uv run ksproject android genkey --dname "CN=My App, O=My Org"
Note
You can also append --dname "CN=My App, O=My Org" to either command if you need to provide a specific Distinguished Name string for the certificate.
The orchestrator automatically detects your built release binary and signs it. Using explicit flags:
# Sign the APK
uv run ksproject android sign --keystore my-release-key.jks --storepass SecretPass123 --keyalias myapp
# Sign an App Bundle (.aab)
uv run ksproject android sign --bundle --keystore my-release-key.jks --storepass SecretPass123 --keyalias myappUsing your .env file:
# Sign the APK
uv run ksproject android sign
# Sign an App Bundle (.aab)
uv run ksproject android sign --bundle
Note
By default, ksproject android sign targets the release directory. You can optionally pass --variant debug to sign a debug artifact, or pass --keypass <password> if your security profile requires an alias password distinct from your core keystore storage password. Similarly, you can append --variant release to the run pipeline to run production builds.
When you run init, ksproject creates two highly important template files in your root directory. The build engine uses these to generate the final project_dist/ payload.
AndroidManifest.tmpl.xml: Add custom broadcast receivers, specific hardware features, or specific intent filters here.ksprojectwill automatically inject your Python services, meta-data, and permissions at build time.build.tmpl.gradle.kts: Modify your core Gradle setup. Need custom Maven repositories orexternalNativeBuildflags? Put them here.- Note on Push Notifications: The default template automatically maps the system environment variable
ONESIGNAL_APP_IDto an Android String Resource (onesignal_app_id), making it instantly available for libraries likepyonesignal.
Your pyproject.toml is the single source of truth for your app's metadata.
[tool.uv]
index-strategy = "unsafe-best-match"
find-links = ["./wheelhouse"]
[tool.uv.pip]
extra-index-url = [
"https://pypi-index.psychowaspx.workers.dev/simple",
"https://pypi.anaconda.org/kivyschool/simple",
]
find-links = ["./wheelhouse"]
#### kivy-school configuration ####
[tool.kivy-school]
app_name = "Hello World"
[tool.kivy-school.android]
package_name = "org.example.hello_world"
# version_name = "0.1" # change when you want to release a very new version
version_code = 1 # keep increasing to make an update to existing version_name (bug fixes/security releases)
# icon = "relative to pyproject (png)"
# presplash_color = "#FFFFFF"
# presplash = "relative to pyproject (png,jpg,gif)"
# presplash_lottie = "relative to pyproject (json)"
archs = [
"arm64-v8a",
# "x86_64" # Uncomment to support emulators (increases APK size)
]
# API and Toolchain Versions
api = 36
min_api = 24
sdk = "36"
ndk = "28c"
ndk_api = 24
# --- Toolchain Management ---
global_tools = true # Set to false to use project-local SDK/NDK (./.kivyschool)
# global_tools_path = "~/.kivyschool" # Override root path when global_tools = true
# --- Gradle & Manifest Configs ---
gradle_dependencies = [
# "com.onesignal:OneSignal:[5.6.1, 5.9.99]"
]
gradle_plugins = [
# 'id("com.google.gms.google-services") version "4.4.2" apply false'
]
permissions = [
# "POST_NOTIFICATIONS", "INTERNET", "ACCESS_NETWORK_STATE"
]
# --- File Inclusions ---
# Format: [Destination, Source(s)]
# include_files = [
# ["gradle/app", ["./google-services.json", "./some-other-config.xml"]]
# ]
# pre_build = "path/to/android_prebuild.py"
# post_build = "path/to/android_prebuild.py"
byte_compile_python = true
# <meta-data> entries inside <application>
# [tool.kivy-school.android.meta_data]
# "com.google.android.gms.ads.APPLICATION_ID" = "ca-app-pub-xxxxxxxx~xxxxxxxx"
# --- Background Services ---
# [[tool.kivy-school.android.services]]
# name = "MyService1"
# start_type = "START_NOT_STICKY"
# entrypoint = "hello_world.services.myservice1"
# foreground = true
# foreground_service_type = "location|dataSync"
# notification_title = "MyService1 Running"
# notification_text = "Service is managing background data."
# notification_icon = "stat_notify_sync"
#### iOS Configuration (Coming Soon) ####
[tool.kivy-school.ios]
bundle_id = "org.example.hello_world"
info_plist = {}
entitlements = {}
permissions = []
frameworks = []
# developer_team = "ABC123XYZ"
| Command | What it does |
|---|---|
ksproject init |
Scaffold a new project, template files, and pyproject.toml configurations |
ksproject android build [debug|release] |
Build an APK artifact |
ksproject android build --aar [debug|release] |
Build an AAR library instead of an APK |
ksproject android build --bundle [debug|release] |
Build an AAB (Android App Bundle) instead of an APK |
ksproject android build [debug|release] --clean |
Perform a clean step before executing the build task |
ksproject android sign --keystore <file> --storepass <pass> --keyalias <alias> |
Sign the built APK inside the target directory using apksigner |
ksproject android sign --bundle --keystore <file> --storepass <pass> --keyalias <alias> |
Sign the built App Bundle (.aab) inside the target directory using jarsigner |
ksproject android genkey --out <file> --storepass <pass> --keyalias <alias> |
Generate a secure Java Keystore (.jks) using keytool |
ksproject android get-path [sdk|ndk|emulator] |
Print the fully resolved local file system path for the specified tool |
ksproject android devices |
List connected active adb serial devices and available AVD profiles |
ksproject android run --name <AVD> |
Build, install, and launch the target application on a simulated emulator |
ksproject android run --uuid <serial> |
Build, install, and launch the target application on a physical USB device |