Skip to content

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-platform to target the correct OS/arch (e.g. aarch64-linux-android)
  • --index-strategy unsafe-best-match so 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

site_packages/<arch>/.kotlin/
└── com/
    └── example/
        └── KotlinHelper.kt

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

site_packages/<arch>/.gradle/
├── com.example.firebase.json
└── org.kivyschool.camera.json

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:

[tool.uv.sources]
my-local-lib = { path = "../my-local-lib" }

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