Packaging and Distribution
Shipping a Kivy app means turning the Python source plus the KV files and assets into something a user can install and run - a desktop executable, an Android APK, or an iOS app. Kivy's tooling splits by target: desktop builds with pyinstaller (the standard Python executable builder, which handles Kivy's native dependencies with a few flags), Android with buildozer (the Kivy project's own tool that drives the Android SDK and produces an APK), and iOS with kivy-ios (the companion to buildozer for the Apple toolchain). This chapter covers the desktop path in depth, the buildozer path for Android, and the asset and data considerations that affect every target.
Assets First: What the Build Must Carry
Before any tool, the app must declare its data. A Kivy app's non-Python files - the .kv files, the images, the fonts, the data files - are the assets the build must bundle, and the two rules that prevent 90% of "works in dev, blank on the device" bugs are: (1) reference assets by a path the build can resolve (the app's data directory, resolved at runtime as the storage chapter's FileSystemStorage does, not a path relative to the source tree), and (2) list every asset directory in the build config so the tool copies it. A KV file that loads data/logo.png by a source-relative path works when the working directory is the project root and fails when the app runs from the installed location - the fix is to resolve the asset path at runtime against the app's base directory. Getting the asset paths right is the prerequisite every packaging tool depends on, so it comes first.
Desktop: PyInstaller
# the standard Kivy desktop build
pip install pyinstaller
# a spec-driven build is more reliable than the one-liner:
pyinstaller --name MyApp \
--windowed \
--add-data "main.kv:." \
--add-data "data:data" \
main.py
# --windowed: no console window (a GUI app)
# --add-data "src:dest": bundle the KV and the asset directory
# the output is dist/MyApp/ (a folder) - zip it, or use --onefile
PyInstaller analyzes main.py, bundles the Python interpreter and the imported packages (Kivy and its native libraries included), and produces a standalone executable. The Kivy-specific flags are the --add-data entries for the .kv and the data directory (the asset rule above), and --windowed to suppress the console. Two Kivy-specific gotchas: the hidden imports - Kivy loads some modules by name at runtime, and if PyInstaller misses one the app fails on a missing module at startup, which is fixed with --hidden-import for the named module - and the --onefile trade, where a single executable extracts to a temp dir on each start (slower launch, cleaner distribution) versus the folder output (faster launch, a directory to ship). The spec file (MyApp.spec) is the durable form of the flags, and it is what a repeatable build script drives - the one-liner is for the first build, the spec is for the release.
Android: Buildozer
# the Kivy Android build tool
pip install buildozer
buildozer init # creates buildozer.spec from a template
# edit buildozer.spec:
# title = My App
# package.name = myapp
# package.domain = com.example
# source.dir = .
# android.permissions = INTERNET # for the networking chapter
# android.api = 34
# android.ndk-api = 23
buildozer android debug # build a debug APK
buildozer android release # a signed release (needs the keystore config)
Buildozer is Kivy's Android pipeline: it manages the Android SDK and NDK, compiles the Python code with the Kivy-based Python-for-Android toolchain, and packages the result into an APK. The buildozer.spec is the config - the app title, the package name and domain (the reverse-DNS identifier), the source directory, the permissions (the INTERNET permission is what the networking chapter's requests need), and the API levels. buildozer android debug produces a debug APK you can install on a device; buildozer android release signs it with your keystore for distribution. The first build is slow (it downloads and builds the toolchain); subsequent builds are incremental. The model is the same as the desktop path - the source plus the assets plus a config - with the Android SDK as the extra layer buildozer manages for you, and the permission list as the Android-specific declaration the desktop build does not have.
iOS: kivy-ios
iOS builds use kivy-ios, the companion tool to buildozer, and it requires the Xcode toolchain (so the build runs on macOS). The flow is the same shape: a recipe-based config (the ios_recipe and the app's recipe) declares the source, the assets, and the dependencies, and kivy-ios build compiles the Python and the Kivy native layer against the iOS SDK, producing an .app you sign and submit with Xcode. The practical note: the iOS path is the most toolchain-dependent of the three (Xcode version, the signing certificates, the device vs simulator target), so it is the one to set up on a dedicated build machine with the toolchain pinned - the same "a repeatable build environment" principle as the desktop spec, with more moving parts. The app code is unchanged across the three targets; only the build tool and the config differ, which is the cross-platform promise the Kivy overview made.
The Working Rules
- Resolve asset paths at runtime (the app's data directory), never source-relative - the build changes the working directory, and a source-relative path is the blank-screen-on-device cause.
- List every asset in the build config - the
--add-dataentries, thesource.dircontents - so the bundle carries the KV and the images. - Use a spec/config for the release build - the one-liner flags are for the first build; the spec file (desktop) or
buildozer.spec(Android) is the repeatable, reviewable artifact. - Declare the permissions the app needs (Android
INTERNET, the iOS usage strings) - a missing permission is a silent network failure on the device, not an error. - Test the built artifact, not just the source - run the packaged app on the target (the emulator for Android, the device for iOS) before shipping, because the build is where the path and permission bugs live.
Summary
Packaging is the source plus the assets plus a config, per target. First get the assets right - resolve paths at runtime against the app's data directory and list every file in the build config, because a source-relative path is the "blank on the device" bug. Desktop builds with PyInstaller (--windowed, the --add-data entries for the KV and data, the hidden imports Kivy loads by name, and the spec file as the durable build). Android builds with Buildozer (buildozer.spec for the title, package, and permissions, buildozer android debug/release for the APK, the SDK and NDK managed by the tool). iOS builds with kivy-ios on the Xcode toolchain, the most toolchain-dependent of the three. The app code is the same across all of it - the cross-platform promise holds - and the rule that keeps every target honest is to test the built artifact on the target, because the build is where the path and permission failures surface.