Site-Packages & Native Assets on Android¶
This page explains how ksproject handles Python packages, native libraries, Java sources, and Gradle configuration during Android builds — specifically how the .libs/, .java/, .kotlin/, and .gradle/ directories in site-packages work.
How Site-Packages Are Installed¶
When you run ksproject android build, the build system installs your project's Python dependencies cross-compiled for each target architecture using uv pip install with platform-specific flags.
For each arch in your archs list (e.g., arm64-v8a, x86_64):
project_dist/gradle/site_packages/<arch>/
├── kivy/ # Cross-compiled Kivy
├── myapp/ # Your app code
├── requests/ # Pure Python dependencies
└── ... # All other dependencies
The uv pip install command uses:
--python-platformto target the correct OS/arch (e.g.aarch64-linux-android)--index-strategy unsafe-best-matchso all configured indexes compete- Your project's
[tool.uv.pip]config — the KivySchool wheel indexes plus your local wheelhouse
At build time, Gradle stages these directories together with the Python stdlib and lib-dynload, optimizes them, and packs everything into a single assets.zip inside the APK.
The Dot-Directory Convention¶
After pip installs all packages, ksproject scans for special dot-prefixed directories that packages inject via custom build backends (ksp-builder, pyjnius-builder). These directories are handled specially during the Gradle build instead of being bundled as Python assets.
.java/ — Java Sources¶
site_packages/<arch>/.java/
├── org/
│ └── example/
│ └── MyHelper.java
└── com/
└── library/
└── Bridge.java
What happens: All Java files under .java/ are copied into the Gradle project's app/src/main/java/ directory. They are then compiled alongside the app's own Java code (MainActivity, services, etc.) during the Gradle build.
Use case: Packages that need JNI bridges or custom Android Java code — for example, a Python wrapper around an Android API that requires a Java companion class.
How they get there: The pyjnius-builder build backend injects Java source files from [tool.pyjnius].java-paths into the wheel under .java/.
Project-level Java
Your own project can also carry Java sources: ksproject init creates a .java/ directory at the project root, and anything you put there is picked up the same way.
.libs/ — Pre-compiled Native Libraries¶
site_packages/<arch>/.libs/
├── arm64-v8a/
│ ├── libcustom.so
│ └── libbridge.so
└── x86_64/
├── libcustom.so
└── libbridge.so
What happens: Native .so files are copied to app/src/main/jniLibs/<abi>/ in the Gradle project. They are packaged into the APK and available at runtime via System.loadLibrary() or Python's ctypes.
Use case: Pre-compiled C/C++/Rust libraries that Python packages depend on at runtime (e.g., OpenSSL, custom native extensions).
How they get there: Packages include .libs/<abi>/*.so in their wheel distribution.
.kotlin/ — Kotlin Sources¶
What happens: Kotlin source files are copied into the Gradle project and compiled alongside Java sources.
Use case: Packages that provide Kotlin-based Android components.
.gradle/ — Gradle Configuration¶
Each JSON file contains:
{
"package_name": "com.example.firebase",
"gradle_dependencies": [
"com.google.firebase:firebase-analytics:21.0.0",
"com.google.firebase:firebase-messaging:23.0.0"
],
"permissions": [
"INTERNET",
"WAKE_LOCK"
]
}
What happens: ksproject reads all .gradle/*.json files, merges their gradle_dependencies and permissions lists (deduplicating), and injects them into the generated app/build.gradle.kts and AndroidManifest.xml.
Use case: Packages that require specific Gradle dependencies or Android permissions to function correctly on Android.
How they get there: The ksp-builder build backend reads [tool.kivy-school.android] from the package's pyproject.toml and injects a .gradle/<package_name>.json file into the wheel.
The Merge Process¶
During build, ksproject performs a multi-source merge of all configuration:
┌─────────────────────────────┐
│ Your pyproject.toml │
│ [tool.kivy-school.android]│
│ • permissions │
│ • gradle_dependencies │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ site_packages/<arch>/ │
│ .gradle/ │
│ • pkg1.json │
│ • pkg2.json │
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Final Merged Config │
│ • All permissions (dedup) │
│ • All dependencies (dedup)│
└──────────────┬──────────────┘
│
▼
┌─────────────────────────────┐
│ Generated Gradle Files │
│ • app/build.gradle.kts │
│ • AndroidManifest.xml │
└─────────────────────────────┘
This means:
- You declare permissions/dependencies in your
pyproject.toml - Your dependencies declare their own via
.gradle/*.json - Everything is merged automatically — no manual configuration
Gradle Custom Tasks¶
The generated app/build.gradle.kts includes custom Gradle tasks that stage, optimize, and package the Python content at build time:
| Task | Purpose |
|---|---|
stagePython_<abi> |
Copy per-arch site-packages (minus dot-dirs), stdlib, and lib-dynload into a staging dir |
optimizeStaged_<abi> |
Byte-compile, strip junk files/dirs, and llvm-strip native libs (details) |
postBuildHook |
Run your post_build script on the staged content (if configured) |
zipPythonAssets |
Pack the staged content into a single assets.zip asset |
copySitePackagesJava |
Copy .java/ sources into src/main/java/ |
copySitePackagesKotlin |
Copy .kotlin/ sources into src/main/kotlin/ |
copySitePackagesNativeLibs_<abi> |
Copy .libs/<abi>/*.so to jniLibs/<abi>/ |
These tasks are wired into preBuild, ensuring all injected sources and libraries are available before Gradle compiles and packages.
How the APK Is Structured¶
The final APK contains:
app-debug.apk
├── classes.dex # Compiled Java/Kotlin (MainActivity, SDL2, custom)
├── lib/
│ └── arm64-v8a/
│ ├── libpython3.so # CPython runtime
│ ├── libSDL2.so # SDL2 (pre-built, shipped with CPython)
│ ├── libmain.so # Native bootstrap (main.c)
│ ├── libservice_main.so # Native bootstrap for services
│ └── libcustom.so # Any .libs/ native libraries
├── assets/
│ └── assets.zip # python3.13 stdlib + lib-dynload + site-packages
├── res/
│ └── mipmap/ic_launcher.png # App icon
└── AndroidManifest.xml # Merged manifest with all permissions
The Python runtime extracts/reads assets.zip at startup; bundling one compressed archive instead of thousands of loose asset files keeps the APK smaller and installs faster.
Workspace & Path Dependencies¶
ksproject also handles local/workspace dependencies (packages referenced by path in your pyproject.toml). It recursively follows all path and workspace dependencies to collect their dot-directories too.
For example, if your project depends on a local package:
And my-local-lib has Java sources in .java/, ksproject will discover and include them in the build — even though they weren't installed via pip into site-packages.
Summary¶
| Directory | Destination in Gradle Project | Compiled Into |
|---|---|---|
.java/ |
app/src/main/java/ |
classes.dex |
.kotlin/ |
app/src/main/kotlin/ |
classes.dex |
.libs/<abi>/ |
app/src/main/jniLibs/<abi>/ |
lib/<abi>/ in APK |
.gradle/*.json |
Merged into build.gradle.kts + manifest |
Build config |
| Site-packages (Python) | Staged + optimized at build time | assets/assets.zip in APK |