Release notes¶
This page is the kivyforge changelog. Entries under Unreleased describe
changes on the main branch that are not yet in a tagged release.
Where this page comes from
This page is generated from CHANGELOG.md in the kivyforge repository when
the site is built. Links to design documents and other repository files open
on GitHub.
[Unreleased]¶
iOS: doctor checks the Team ID¶
- Added:
kivyforge doctor -p ioshas a "Team ID" check (#108). It reads the Team ID that--deviceand--releasebuilds sign with, fromKIVYFORGE_TEAM_IDor[tool.kivy.ios.signing].team_id, and compares it with the teams of the signing certificates in your keychain. The check: - fails when the value is not 10 capital letters and digits;
- fails when the value is the ID in parentheses in an
Apple Developmentcertificate's name, which is not a Team ID but is often pasted as one, and names that certificate's Team ID instead; - fails when a pinned
provisioning_profileis for a different team; - warns when no certificate in your keychain is for that team, because automatic signing can still create one during the build.
Before, a wrong Team ID failed only at the end of the build, inside
xcodebuild. With no Team ID set, the check is skipped, since simulator builds
don't sign, and it lists the Team IDs in your keychain, each with its team's
name, so you can copy one. See
Find your Team ID.
- Migration: none needed. doctor now fails a project whose Team ID would
have failed the build; build itself is unchanged.
Guides¶
- Added: Sign your iOS app
explains how to find your Team ID
(#108). With a paid
membership it is on the Apple Developer website. A free Apple ID's personal
team has no page there, so the guide shows how to read the ID from the
team's
Apple Developmentcertificate in Keychain Access, and warns that the ID in parentheses in the certificate's name is a different value.
v3.0.0.dev1 - 2026-10-14¶
All platforms: the app's own package metadata¶
- Added: kivyforge writes each app's package metadata into the app, from
[project](#122): a<name>-<version>.dist-infofolder beside the app's code on macOS, Linux, Windows, Android and iOS. Inside the app,importlib.metadatanow finds the app's name, its version (the same value asversionNameandCFBundleShortVersionString), the entry points of[project.scripts],[project.gui-scripts]and[project.entry-points], and its modules throughpackages_distributions(), for every build backend and layout, with or without a development install. Before, an app had metadata only if a setuptools dev install had left anegg-infoinapp_dir, as old as that install. See Read your package metadata inside the app. - Changed: a development
*.egg-infofolder at the top ofapp_dir, and a.dist-infofolder of the app's own name there, no longer ship in the app; the generated folder replaces them. Your source tree is not changed. On iOS,kivyforge buildstages the folder in<app>-ios/_kivyforge_meta/and the generated Build Python script copies it into the bundle, so Xcode-only rebuilds keep it. Migration: an app that read its metadata from theegg-infonow reads the same names from the generated folder, current with the build; nothing to change. An app whose code looks for theegg-infofolder itself should useimportlib.metadatainstead. - Changed: a setuptools project that declares entry points only in
setup.cfg([options.entry_points]) orsetup.py(entry_points=), and none in[project], failsbuild,packageandrunwithKF-METADATA-ENTRY-POINTS-OUTSIDE-PROJECT(exit 1). kivyforge writes the metadata from[project], so the app would otherwise start without them. Migration: move them into[project.entry-points], console scripts into[project.scripts]and GUI scripts into[project.gui-scripts]. setuptools has deprecated reading them from those files for a project with a[project]table. - Changed:
[project]entry points and[project].nameare checked as build backends check them, and each violation is a config error at its line in every verb: aconsole_scriptsorgui_scriptsgroup under[project.entry-points], a nested table (usually a dotted group name left unquoted), an invalid group name, an entry point name with=or a leading[or whitespace, and a project name that is not valid. Migration: pip could not have built such a project either; fix the table the message names. - Added: when
[project].dynamiclistsentry-points,scriptsorgui-scripts, kivyforge cannot know the entry points, so it generates nothing, leavesapp_diras it is, and warns withKF-METADATA-NOT-GENERATED. Itscontextnames thereasonand the developmentegg-infothat ships instead (fallback). - Added: Android's contract self-test, run by
run --smoke, checks that the app finds the metadata kivyforge generated.
iOS: rebuilding in Xcode keeps a device build signed¶
- Fixed: rebuilding a device build in Xcode (Cmd-B or Cmd-R, or re-running
the
xcodebuildcommand kivyforge ran) succeeded but left the.appwith an invalid signature:codesign --verify --deep --strictreported "a sealed resource is missing or invalid" and named everyFrameworks/<module>.frameworkbinary. The Build Python script re-signs those frameworks on every run, and with a real signing identity that changes their bytes. Xcode didn't know the script writes into the bundle, so it didn't sign the app again, and the app's signature still described the old bytes. The script now declares what it writes (python/,pip-deps/andFrameworks/) as its outputs, and Xcode signs the app again after it. That adds about a second to an incremental build. The app from such a build still installed and ran on iOS 27, but anything that checks its signature rejects it. Simulator builds, signed ad hoc, stayed valid. A project generated by an earlier kivyforge gets the fix at its nextkivyforge build, which regenerates the script phase.
iOS: the default deployment target is 16.0¶
- Fixed: with Xcode 27,
kivyforge build -p iosfailed for any project that did not set[tool.kivy.ios].deployment_target. The default was 13.0, and Xcode 27 accepts only 15.0 to 27.0.x, so xcodebuild stopped at the end of the build ("The iOS deployment target 'IPHONEOS_DEPLOYMENT_TARGET' is set to 13.0, but the range of supported deployment target versions is 15.0 to 27.0.x";KF-BUILD-TOOL-FAILED, exit 5). The default is now 16.0, not Xcode's 15.0, because that is the oldest iOS Kivy's own iOS wheels run on: their extension modules are built for iOS 16.0, so a lower target lets older devices install an app that cannot load Kivy.kivyforge initwrites 16.0, and thehello-worldexample, the one example still at 13.0, now uses 16.0. - Fixed: the generated Xcode project always had 13.0 as its project-level
IPHONEOS_DEPLOYMENT_TARGET, whateverdeployment_targetsaid. Only the app target had the configured value. Both levels now use it, and the next build of an existing<app>-ios/project corrects the old value. - Added:
kivyforge doctor -p ioshas a "Deployment target" check. It reads the range the selected Xcode accepts from the iOS SDK and fails whendeployment_targetis outside it, before a build gets that far. Without a project, it reports the range. - Migration: what to do depends on how your project sets the key.
- Set below 15.0: Xcode 27 refuses the value whatever kivyforge's
default, so raise it (to
"16.0"for a Kivy app). Any edit topyproject.tomlmakesbuildstop withKF-LOCK-DRIFT(exit 4), so runkivyforge lock -p iosafter the edit. - Not set: you need no edit, but your app's minimum iOS rises from 13.0
to 16.0 (
MinimumOSVersionandIPHONEOS_DEPLOYMENT_TARGET), so devices below iOS 16 cannot install new builds. If your dependencies run on iOS 15 (Kivy's do not), setdeployment_target = "15.0", the lowest value Xcode 27 accepts. - Existing locks stay valid. The lock checks a hash of the text of
pyproject.toml, and a default is not in that text, sobuild,packageandlock --checkaccept a lock made before this change. A re-lock does ask pip forios_16_0_*tags instead ofios_13_0_*. Wheels built for an older iOS still match, so the kivy and pyobjus wheels (taggedios_13_0) resolve to the same files. A package that publishes wheels for several minimum iOS versions may resolve to a different file, which makeslock --checkreport the lock as stale; runkivyforge lock -p ios --update.
Guides¶
- Added: Manage dependencies and re-lock
explains how to pin a package with an exact specifier
(#107). A re-lock,
including one forced by an unrelated edit to
pyproject.toml, resolves every specifier to its newest match.lock --checkdoes that resolve even when the file hash still matches, so a range fails the check when a newer release appears.buildandpackagekeep accepting that lock until the hash changes. - Fixed: Install kivyforge now explains that every release so far is a pre-release, and its pipx and pip commands carry the pre-release flag. Without it, both fail to find kivyforge.
Android¶
- Added:
kivyforge lock -p androidshows a progress bar while it downloads the python.org CPython runtime, one per ABI, the first time that tarball is not already cached (#84). Before, that download printed nothing, so a first lock looked stuck until[lock] <abi>: resolving wheels. - Changed:
kivyforge build -p androidwithout--debugnow ends with a line saying Gradle did not run and naming the commands that produce an APK (#106). Before, it ended onGenerated <app>-androidwith exit 0 and no APK, which read like a successful build. What a bare build does is unchanged: it generates the project and stages the release payload forpackage. If you run./gradlew assembleDebugyourself on that project, the debug APK you get contains the release payload. Usebuild --debuginstead.--jsonoutput is unchanged.
All platforms: dynamic versions¶
- Added:
dynamic = ["version"]in[project]is supported (#111). A project that takes its version from git through setuptools-scm, hatch-vcs or another build backend plugin no longer fails with "missing or empty [project].version".build,packageandrunask pip to prepare the project's metadata, in an isolated environment, and print the version they got. This works whether kivyforge was installed withuv tool, pipx or as a project dependency.lock,doctor,statusandcleannever run pip, and a new commit doesn't make the lock out of date. The first build needs the network to fetch the backend. See Set the app version. - Added:
--app-version VERSIONonbuild,packageandrun, and theKIVYFORGE_APP_VERSIONenvironment variable (the flag wins), to set the version without running the build backend.--no-build-isolationresolves a dynamic version with a backend installed alongside kivyforge, offline. - Added:
--jsonenvelopes frombuildandpackagecarrydata.version:{"value", "source"}, wheresourceisoverride,staticorbackend. It is recorded before the build starts, so failed builds keep it. - Added:
doctorreports a Version check. New codes:KF-VERSION-UNRESOLVED(exit 1),KF-VERSION-APPLE-FORMAT,KF-VERSION-FILE-NOT-REFRESHED,KF-VERSION-ISOLATION-UNUSED. - Changed: an iOS export for the App Store (
package -p ios, orbuild -p ios --release, both with the default--export-method app-store) fails before Xcode runs when the version isn't one to three dot-separated numbers, such as1.0rc1or1.2.3.4. App Store Connect refuses such a version at upload (ITMS-90060). Other iOS exports and macOSpackagewarn instead, and debug builds are unaffected. Migration: a static pre-release version used to fail at upload; it now fails atpackage. Build from a release version, pass--app-version, or export with--export-method ad-hocordevelopment. - Changed:
[project].versionmust follow PEP 440, and kivyforge writes its normalized form. Migration:v1.2becomes1.2inversionName, the plists and file names;1.0-betabecomes1.0b0; a local segment is lowercased with-turned into.(1.0+Build-5becomes1.0+build.5). A version PEP 440 can't parse, such as1.0.0-SNAPSHOT, now fails to load; pip already refused to build such a project. - Changed: a
+in the version becomes_in Linux.AppImageand Windowsdistfolder names, since+turns into a space in URL query strings. Migration:1.0+localnow packages as...-1.0_local-.... - Changed: listing
name,dependencies,authors,descriptionorrequires-pythonin[project].dynamicis now an error. kivyforge reads those fields itself, and a dynamic one used to load as absent: adynamic = ["dependencies"]project locked no dependencies at all. Migration: set the field statically.
Android: pyjnius versions¶
- Fixed: pyjnius 1.9.0 failed every Android build with "locked pyjnius 1.9.0
is outside the bootstrap template's compatible range"
(#113), though its
invoke0method is unchanged. kivyforge no longer caps the pyjnius version. Instead of guessing from the version number,buildnow checks the installed pyjnius wheel for theinvoke0signature kivyforge's Java glue declares, and fails with a message naming the wheel if it differs. New pyjnius releases that keep the signature work without a kivyforge update. - Changed:
kivyforge lock -p androidaddspyjnius>=1.7.0instead ofpyjnius>=1.7.0,<1.9when your project doesn't declare it, so a re-lock picks the newest release. Existing locks need no change: a lock that already holds pyjnius 1.9.0 now builds. Older than 1.7 still fails, anddoctoronly checks that floor, since the wheel check needs the installed wheel.
Android: wheels built by cibuildwheel¶
- Fixed: numpy, pandas, matplotlib, opencv-python and other wheels that
cibuildwheel's default repair (auditwheel) produces failed to import on the
device with
dlopen failed: library "libc++_shared-d523468d.so" not found. These wheels carry their shared libraries in a top-level<dist>.libs/directory (numpy.libs/), which kivyforge staged as Python extension modules under new names. They are now staged under their own names, like the Kivy wheel's.libs/. - Fixed: once staged, the same libraries were corrupted by the strip during
packaging, and numpy then failed with errors like
cannot find "operty_get" from verneed[0]. patchelf, which auditwheel uses, lays these libraries out in a way the NDK'sllvm-stripmishandles. kivyforge now recognises them and keeps their symbols, and the build says how many it kept. - Added: after Gradle builds an APK or AAB, kivyforge checks every native
library in it and fails with
KF-ANDROID-NATIVE-LIB-MISALIGNEDif one could not be loaded on a device, instead of shipping an app that crashes at import. - Changed: debug builds no longer strip native libraries. The default
strip_native_libs = "release"was meant to keep symbols in debug builds, as documented, but the generated project stripped both. Debug APKs grow by the size of the symbols; setstrip_native_libs = trueto strip debug builds as before. - Changed: a wheel whose libraries directory holds a
.sonot namedlib*.so(for examplelibgfortran-040039e1.so.5) now fails the build with a message naming the file. Android only packageslib*.sofiles, so before this change the library was missing from the app. - Changed: wheels from Flet's index (and others using Chaquopy's separate
library wheels, such as
flet-libjpeg) now fail the build withKF-ANDROID-WHEEL-LAYOUT, naming the wheels. Their libraries live underopt/lib, which kivyforge cannot load from. Before, the build failed with a misleading "ABI content skew" on their.afiles, or, without those, built an app that could not find the libraries. - Added: a native library built for another architecture than its
wheel's tag (an x86_64 binary in an
arm64_v8awheel) now fails the build withKF-ANDROID-WHEEL-ARCH, instead of failing to load on the device. -
Fixed:
run -p android --releasewithout a configured release signing identity failed with "Gradle reported success for assembleRelease but no artifact is at ...". It said it was signing with the debug keystore, but left the APK unsigned; now it signs with the debug keystore. -
Fixed: libraries a package carries inside its own directory, such as
ffmpeg/.libs/libavcodec.soorblosc2/lib/libtcc.so, were staged as Python extension modules under new names, so nothing could find them. They are now staged under their own names, like a top-level.libs/. - Changed: an extension module named with a suffix CPython on Android does
not import (a build host's
.cpython-314-x86_64-linux-gnu.so, or another Python version's) now fails the build withKF-ANDROID-WHEEL-SUFFIX. kivyforge's loader ignored the suffix, so such a wheel used to load under kivyforge while being unusable anywhere else.
Each auditwheel-repaired wheel carries its own renamed copy
of the C++ runtime, so an app can load several. That is safe unless C++ objects
pass between libraries from different wheels; see
docs/design/platforms/android/03-artifact-distribution-android.md.
Wheels from more package indexes¶
- Fixed: wheels hosted on Cloudflare R2, such as the Android Pillow wheels
on the Kivy School index, locked fine and then failed to download at build
time with HTTP 403 (#97).
The host refuses Python's default
Python-urllibUser-Agent. Every request kivyforge makes now identifies itself askivyforge/<version>. - Changed: when an index publishes no SHA-256 for a wheel,
locknow says so and gives the two fixes: use an index that publishes hashes, or put the wheels in afind_linksdirectory, where kivyforge hashes them itself. It used to suggest re-running with network access, which never helps. The requirement itself is unchanged: kivyforge still pins every wheel by SHA-256. This only happens with indexes that serve wheel metadata (PEP 658) but no hashes, because pip then resolves without downloading the wheel.
build shows its downloads¶
- Added: on every platform,
build(andrun's implicit build) now shows a progress bar for each Python runtime and wheel it downloads, or a few percentage lines when the output is a log. These downloads happen on a first build, afterclean --cache, or with--no-cache, and used to be silent. - Changed:
clean --cache's help and the guides now say that pip's own cache, whichlockresolves through, is left alone (pip cache purgeclears it). Afterclean --cache,lockusually stays fast and shows no downloads.
Android: pyjnius is included automatically¶
- Fixed: an Android app whose dependencies didn't list
pyjniusbuilt without complaint and then crashed at launch withNo module named 'jnius'(#65). Kivy imports pyjnius on Android (to read the screen density, for example) but doesn't declare it, because python-for-android always supplied it. Nowkivyforge lock -p androidadds it, in the range kivyforge supports (>=1.7.0,<1.9), when your project doesn't declare it. It is added to the Android lock only, so desktop installs are unaffected and you don't need a; sys_platform == 'android'marker. Declaring it yourself still works and chooses the version. - Fixed: a
pyjniusrequirement whose marker leaves Android out (for examplepyjnius; sys_platform == 'win32') counted as declared, so nothing was added, pip skipped it for Android, andbuildstopped withKF-LOCK-INCOMPLETE, which re-locking did not cure. A declaration now counts only if its marker holds on every ABI being locked; otherwise kivyforge adds its own requirement as well. - Migration: a lock made before this change, without pyjnius, now stops the
build with
KF-LOCK-INCOMPLETE(exit 4) instead of producing an app that crashes. Runkivyforge lock -p android --update. Projects that already declare pyjnius need no change.
Android: lock and build show their downloads¶
- Fixed:
kivyforge lock -p androidprinted nothing until it finished, so a slow connection looked exactly like a hang (#84). It now reports each step, shows each wheel download as a progress bar (or as a few percentage lines when the output is a log rather than a terminal), and says it is still working after 30 s with nothing else to show. The Gradle step for Maven dependencies is announced too. - Added: before Gradle runs, a missing NDK or CMake at the versions the
generated build pins is installed with the SDK's
sdkmanager, with progress (#74). Gradle used to download the NDK itself in the middle of the build, a download of several hundred MB with no progress shown. Withoutsdkmanager, or with its license not accepted,buildsays so and Gradle installs it as before. - Changed: download progress is drawn by the console layer with Rich, so it
is a live bar on a terminal and plain lines in CI logs, redirected output and
under
--no-color. Progress stays on stderr, so--jsonoutput is unaffected.
Android: adding your own resources with include_files¶
- Added: a guide section,
Add Android resources:
stage a folder laid out like an Android
resfolder withdest = "app/src/main/res", keeping subfolders and qualifiers. (#71) - Added: a file that replaces one kivyforge generates, such as
res/values/styles.xmlor the launcher icon, is now reported ([include] ... replaces the file kivyforge generated, andKF-ANDROID-INCLUDE-OVERRIDEunder--json). It used to be silent. - Added: the build stops, saying what to fix, when an included
strings.xmlorstyles.xmlleavesres/values/withoutapp_nameorTheme.Kivyforge, which the generated manifest uses. That used to fail later in Gradle with an AAPT error that did not mentioninclude_files. - Fixed: a file removed from
include_files, or from a directory it names, is now removed from the generated project on the next build. It used to stay, and ship, untilkivyforge clean.
Android: run follows the app's log¶
- Changed:
kivyforge run -p androidnow shows the app's log live, the way pidcat or Briefcase does, until the app exits or you press Ctrl+C (which stops the log and leaves the app running). It used to print one snapshot 25 seconds after launch and return, so anything the app logged later, such as a traceback from a button press, never appeared, and a slow first start could show no Kivy output at all. (#73) - Fixed: the log is now the app's own process (
adb logcat --pid). The old filter matched text in each line, so it let other apps' lines through (any line mentioningSDLorkivyforge) and dropped the app's own Android crash report. - Added: if the app never starts,
runprints the device log since launch and fails, instead of showing an empty capture. - Migration: a script that runs
kivyforge run -p androidand expects it to return must add--no-follow, which keeps the one-snapshot behaviour (now filtered to the app's process).run --smokeis unchanged.
Android: switching kivy_generation no longer needs kivyforge clean¶
- Fixed: rebuilding after changing
[tool.kivy.android].kivy_generationfailed to compile untilkivyforge cleanwas run. Going from 3 to 2 failed withduplicate class: org.libsdl.app.SDLInputConnection, because the SDL3 Java files were left in the generated project. Going from 2 to 3 failed withcannot find symbol: SDLInputConnection, because Gradle's incremental compiler loses a class that SDL3 moved into its own file. The build now removes the other generation's SDL files, and on a generation change it discardsapp/build(stopping the project's Gradle daemon first), so that one build compiles from scratch. Builds that keep the same generation are unaffected. (#66)
Android: apps relaunch on Android 7, and services can use pyjnius below Android 12¶
- Fixed: on Android 7.0 and 7.1 (API 24 and 25), every app crashed on its
second launch, and every service crashed on start, with
NoSuchMethodError: ... toPath(). The check for an already-unpacked bundle usedjava.nio.file, which Android added in 8.0. Only the first launch after an install got through. - Fixed: in a service on Android 7 to 11 (API 24 to 30),
import jniusraisedRuntimeError: pyjnius (Android) could not obtain a JNIEnv. A service now loads the app's SDL library before Python starts, which is how pyjnius finds the Java VM. It creates no window. - Fixed: on Windows,
kivyforge run -p androidcould crash withAttributeError: 'NoneType' object has no attribute 'splitlines'after launching the app. adb output is now read as UTF-8. - Added:
kivyforge run --smokealso checks a relaunch: it unpacks the bundle a second time and expects the already-unpacked bundle to be used. A fresh install never takes that path, which is how the Android 7 crash above went unnoticed.
Android: the android package is bundled¶
- Added: every Android build includes python-for-android's
androidpackage, so code that imports it runs unchanged:android.permissions,android.activity,android.runnable,android.broadcast,android.darkmode,android.storage,android.config, and the package's ownmActivity,api_version,open_urland the rest. The modules are taken unchanged from python-for-android at a pinned revision. plyer's Android facades, which import it, can now load. Nothing to add to your dependencies, but the package needspyjnius. See Use the android package. - Fixed: on Kivy 2.3.1, tapping a
TextInputquit the app, and so did the back key,App.stop()andApp.pause(): Kivy calls the activity through this package. Kivy's Android clipboard provider now loads, and so does its Android audio provider when an app selects it withKIVY_AUDIO. ATextInputwithinput_type = "number"(or"mail","tel", ...) gets the matching keyboard. - Changed:
kivyforge lockfails withKF-ANDROID-DEPENDENCY-CONFLICTwhen a dependency is namedandroid, andkivyforge buildfails withKF-ANDROID-APP-CONFLICTwhenapp_dirhas anandroid.pyor anandroid/package (or withKF-ANDROID-DEPENDENCY-CONFLICTwhen an installed wheel provides one). Both exit 1. The bundled package always comes first, so a second one would ship and never be imported. - Migration: remove
androidfrom your dependencies, for example a package index'sandroidwheel or anandroidcarried over from a buildozerrequirementsline. Rename anandroid.pyorandroid/of your own.
Android: services have start, stop and mService¶
- Added: each generated service class has static
start(context, argument)andstop(context)methods. A foreground service also gets the five-argumentstart(context, icon, title, text, argument), which replaces the configured notification text for that run, and is started withstartForegroundService. Inside a service,autoclass("org.kivy.android.PythonService").mServiceis the running service. These match python-for-android's services, so ported code only changes the class name toorg.kivy.android.Service<name>. See Add a background service and the newexamples/mobile/android-services. - Changed: stopping a service now ends its process. Before, the service stopped but its Python code kept running in the background.
- Migration: none for the intent-based start, which still works. A service that relied on running on after it was stopped has to stay started instead. pyjnius can reach the VM in a service process only on Android 12 (API 31) and later.
Android: a theme switch no longer quits the app¶
- Fixed: switching the phone between light and dark mode quit a kivyforge
Android app, on Kivy 2.3.1 (SDL2) and Kivy 3.0 (SDL3). Android recreates an
activity for any configuration change the manifest does not declare, and SDL
ends the app when its activity is recreated. The generated manifest now
declares the same
android:configChangeslist as python-for-android, includinguiMode, so a theme switch, a language or font-size change, a density change or a split-screen resize reaches the running app instead. - Migration: none. An app that set its own
android:configChangesthrough[tool.kivy.android.manifest].activitykeeps its value.
Android: Kivy 2.3.1 apps keep clear of the system bars¶
- Fixed: on Android 15 and later, an app targeting API 35 or higher is drawn
edge to edge, under the status bar and the navigation bar. Kivy 2.3.1 can't
report a safe area, so a
kivy_generation = 2app's content sat under the bars with no way to avoid them. The generated activity now keeps the window clear of the bars and the notch on API 30 and higher, with black behind the bars and light bar icons. The padding follows rotation, and withfullscreen = trueonly the notch is padded. Kivy 3.0 apps are unchanged: they stay edge to edge and pad themselves withkivy.mobile.get_safe_area()(#63). - Migration: a generation-2 app's window is now smaller by the bar heights (for example 1080x2216 instead of 1080x2400 on a Pixel 8a). An app that padded itself for the bars, for example by reading the insets with pyjnius, should drop that padding.
Android: orientation is honoured at runtime¶
- Fixed: an Android app rotated to any orientation when the phone's
auto-rotate was on, whatever
[tool.kivy].orientationsaid. The manifest declared the right orientation, but SDL replaced it when Kivy created its window: with no orientation hint and a resizable window, SDL allows every orientation. The generated activity now passes[tool.kivy].orientationto Kivy asKIVY_ORIENTATION, the variable python-for-android sets for buildozer apps, so SDL requests what the manifest declares. Applies to Kivy 2.3.1 (SDL2) and Kivy 3.0 (SDL3). - Migration: none for most apps. An app that declares
portraitbut relied on rotating anyway must now list the orientations it supports.
Android: fullscreen hides the system bars¶
- Added:
[tool.kivy.android].fullscreen = trueruns the app with the status bar and the navigation bar hidden. A swipe from the edge shows them for a moment. This is the counterpart of buildozer'sfullscreen = 1, andkivyforge initnow names it when it finds abuildozer.spec. Works on both Kivy 2.3.1 (SDL2) and Kivy 3.0 (SDL3). - Migration: none for existing projects: the default is
false, which is today's behaviour. buildozer treats a missingfullscreenas1, so an app migrated from buildozer that never set the key needsfullscreen = trueto keep hiding the bars.
Android and iOS: a failed download is reported, not a traceback¶
- Fixed: when an Android or iOS build could not download a locked artifact (for
example, a wheel host answering
HTTP Error 503), or the download failed its SHA-256 check, kivyforge crashed with a Python traceback. Under--json, stdout was empty instead of one envelope. Both are now reported as an ordinary error naming the artifact's URL and the reason, as the desktop backends already did:KF-ERRORand exit1, with a--jsonenvelope. - Migration: none needed. A download error is usually transient: retry the build.
Android builds refuse to start Gradle without a JDK¶
- Fixed: a
build -p android --debug(orpackage,run) attempted with only a JRE installed started a Gradle daemon, and that daemon outlived the failed build. After installing a JDK at the same path (as Debian'sopenjdk-21-jdk-headlessdoes), the next build still failed with "does not provide the required capabilities: [JAVA_COMPILER]" untilkivyforge cleanstopped the daemon. Every Android verb that runs Gradle now checks first, the same waydoctordoes, and fails before Gradle starts when there is nojavaor thejavaGradle would run has nojavacbeside it. (Issue #61.) - Changed: those two cases now fail with
KF-TOOLCHAIN-MISSINGand exit3("fix the machine"), withcontext.toolset tojavaorjavac. Before, Gradle ran and failed withKF-BUILD-TOOL-FAILEDand exit5. A JDK of an unsupported version is still reported by Gradle, as before. - Migration: a script that treated exit
5as "no JDK" should check for exit3andKF-TOOLCHAIN-MISSING. If a daemon from an earlier kivyforge release is already stuck, run./gradlew --stopin<app>-android/once (orkivyforge clean).
doctor -p android: fail a JRE with no javac¶
- Fixed: with only a JRE installed (for example Debian's
openjdk-21-jre-headless),kivyforge doctor -p androidreported the JDK check as passed, because the version is in range. The build then failed atcompileDebugJavaWithJavacwith "does not provide the required capabilities: [JAVA_COMPILER]". The JDK check now also requires ajavacbeside thejavaGradle will run (following symlinks, as Debian's/usr/bin/javaneeds), and fails with a hint to install a full JDK. (Issue #47.) - Migration: none needed. If
doctornow fails the JDK check, install a JDK (sudo apt install openjdk-21-jdk-headlesson Debian or Ubuntu) and pointJAVA_HOMEat it.
Android: BOMs and Gradle plugins (enables Firebase)¶
- Added:
[tool.kivy.android.gradle].platformslists BOM coordinates, such ascom.google.firebase:firebase-bom:34.15.0. Each is imported with Gradle'splatform(), and adependenciesentry may then omit its version (com.google.firebase:firebase-analytics).kivyforge lockrecords the version the BOM chose for each library, with its SHA-256. The BOM needs an exact version. - Added:
[tool.kivy.android.gradle.plugins]maps a Gradle plugin ID to an exact version, for example"com.google.gms.google-services" = "4.4.2". Plugins are applied after the Android plugin, in the order listed. Withinclude_filesstaginggoogle-services.json, this is everything Firebase needs. A plugin's version is pinned, but no hash is recorded for it, the same as for the Android Gradle plugin. - Migration: none needed. Projects without these keys generate the same Gradle files and the same lock as before.
doctor: no more iOS failures off a Mac¶
- Fixed:
kivyforge doctorwith no-pon a project whose only overlay is[tool.kivy.android](or any single non-host platform) fell back to iOS and reportedmissing [tool.kivy.ios] table,Xcode not foundandxcode-select path not set, none of which the user can or should act on. It now uses the project's only configured platform, and for a bare environment check or an ambiguous project on Windows or Linux, the host's own platform. iOS remains the fallback on macOS. - Migration: none needed. Pass
-por setKIVYFORGE_PLATFORMto pick a platform explicitly, as before.
Android: SDK license guidance for command-line tools 23+¶
- Fixed: the
doctor"SDK licenses" hint, the README, and the Android quickstart told you to runsdkmanager --licenses. In command-line tools 23 and latersdkmanageris a wrapper around Google's Android CLI, and--licensesdoes nothing (exit 0, no license recorded), so the warning never cleared. The hint now says to install any package (such assdkmanager platform-tools), which records the license;--licensesis still named for older tools. - Docs: the quickstart has a new "SDK licenses and
sdkmanager" section. It also notes that the newsdkmanagerexits 0 when an install fails, and that the Android CLI collects usage data unless run with--no-metrics. - CI: the jobs that install the pinned NDK, CMake, platform, and build-tools now assert each package landed, because a failed install no longer fails the step.
- Migration: none needed.
Android: doctor checks CMake¶
- Added:
kivyforge doctornow checks for the CMake the native launcher compile needs. It used to be missing from the checks, so Gradle downloaded it silently during the first build. A missing CMake is a warning with the exactsdkmanager 'cmake;3.22.1'command. - Changed: the generated
app/build.gradlepinsexternalNativeBuild.cmake.versionto3.22.1(AGP's own default), so the build anddoctorask for the same package. - Migration: none needed. A build still downloads CMake itself when it is missing and the SDK licenses are accepted.
Android: compile Java at level 17¶
- Changed: the generated
app/build.gradlenow setssourceCompatibilityandtargetCompatibilitytoVERSION_17(wasVERSION_1_8). JDK 21+ reports source/target 8 as obsolete on every build; kivyforge already requires JDK 17+, so no new JDK is needed. - Migration: none needed. Regenerate the project (
kivyforge build) to pick it up.
Android: accept pyjnius 1.8¶
- Changed: the bootstrap's pyjnius
invoke0range is now>=1.7.0,<1.9(was<1.8). pyjnius 1.8.0 is the first release with Android wheels on PyPI, so a barepyjniusdependency re-locks to it, andbuildanddoctorused to reject that lock. - Fixed: the
build/doctormessage for an out-of-range pyjnius said to re-lock, which picks the same version again. It now says to constrain pyjnius in[project].dependencies. - Migration: none needed. Projects locked to 1.7.x keep working.
lock (Android): warn when kivy_generation disagrees with the locked Kivy¶
- Added:
kivyforge lock -p androidnow warns (and records aKF-LOCK-WARNINGdiagnostic under--json) when[tool.kivy.android].kivy_generationdoes not match the Kivy it resolved, for examplekivy_generation = 3locking Kivy 2.3.1. Previously onlydoctornoticed, after the lock was written. The lock is still written. - Docs:
kivy_generationselects the SDL bootstrap only; it never influences resolution. The Kivy version comes from[project].dependencies, so Kivy 3.0 needs a requirement that admits it, such as"kivy>=3.0.0.dev0,<4".
init: documented [tool.kivy], and templates that fit each platform¶
- Changed:
[tool.kivy]now carries a comment on every key.orientationis written only when a mobile platform is being seeded, since only the iOS and Android backends read it. Seeding several platforms at once now writes iOS and Android first, so the shared table keepsorientation. - Changed: the
excludelist in the desktop (macOS, Linux, Windows) templates is now an optional, commented-out size trim. It used to be active and droppedrequestsand its dependencies, which breaksUrlRequest. iOS and Android keep the active list. - Changed: the iOS and Android templates write
extra_index_urlsfor the kivy-mobile-wheels index uncommented. Without itkivyforge lockcannot find a Kivy wheel.initnow assumes Kivy is a dependency: theexcludelist and this line are always written, instead of only when[project].dependenciesnameskivy. The Linux template notes thatarchs = ["aarch64"]targets a Raspberry Pi. - Fixed: the "no pyproject.toml" and "found requirements.txt" messages
suggested
kivy>=3.0, which has no desktop wheel yet. They now suggestkivy>=2.3.1and say an iOS app needs Kivy 3.0. - Migration: existing projects are untouched. A project that relied on the
desktop
excludedefault should add it back by hand.
lock: the resolver error names the package and says what to do¶
- Changed: when pip cannot resolve a dependency for a mobile or desktop
target,
kivyforge locknow names the package in its own headline (for exampleno android_24_arm64_v8a wheel for 'numpy') instead of leaving it in pip's output further down. For Android it first suggests the first-party kivy-mobile-wheels index (extra_index_urls) when it is not configured, and otherwisefind_linksor dropping the dependency. The iOS error gives the same index hint. Error codes are unchanged. - Docs: the README's Android quick start now explains that Kivy has no
Android wheel on PyPI, shows the
extra_index_urlsline, and sayslockuses kivyforge's own pip, not the app environment's. Theinittemplate comment for that line no longer says "until they are on PyPI".
run: progress goes to stderr¶
- Changed:
kivyforge runprinted its own progress ([run]/[smoke]lines,Installing on ...,Launching ..., and every line of the build it runs first) to stdout. That now goes to stderr, as it does forbuildandpackage. stdout carries only what the command produces: the app's own output on desktop, the app's filtered logcat on Android, the app console on iOS, and "Contract smoke test PASSED." for--smoke. On iOS the app's output arrives as Apple's launch tool delivers it: the simulator merges the app's stdout and stderr onto stdout, while a device keeps them apart, so Kivy's log (which Kivy writes to stderr) shows on stderr, anddevicectladds two lines of its own to stdout. - Migration: a script that captured
run's stdout to read build progress should read stderr instead. One that only checked the exit status, or read the app's output or the smoke verdict, is unaffected.
Android: App Bundles are now checked in CI¶
- Google Play requires an App Bundle (
.aab) for new apps, but no CI job built one: every Android job packaged an APK, one ABI at a time. A newandroid_bundlejob now runskivyforge package -p android -f aabwith both default ABIs and checks the result: every ABI complete and shipping the same native libraries, the byte-compiled payload, the merged manifest, the bundle's signature (jarsigner -verify), and that the APKs Play generates will still extract native libraries, which kivyforge's Python loader requires. - The post-build checks now understand packages with more than one ABI. A correct two-ABI package used to fail them.
- Test-only; no change to what kivyforge builds.
Android doctor checks the JDK's version and finds the SDK on macOS¶
- Fixed:
doctor -p androidpassed anyjavathat existed. On a Mac with no JDK, Apple's/usr/bin/javastub passed, and so did Android Studio's bundled JDK 25, which the pinned Gradle 8.11.1 cannot run on. The build then failed with "Unsupported class file major version".doctornow runsjava -versionand requires a JDK from 17 to 23. - Fixed: with
ANDROID_HOMEunset, kivyforge did not look in~/Library/Android/sdk, where Android Studio installs the SDK on macOS.doctor, the build'ssdk.dirandrun'sadblookup now find it. - The docs said "JDK 17 or later". The range is now stated once, in the
Android supported-versions table, which the other pages link to. Those
pages note that Android Studio's bundled JDK may fall outside it and that
doctorchecks. - Fixed:
run -p android --smokewithout--releasestaged the release payload, so with the defaultbyte_compile = "release"it tested byte-compiled, stripped Python inside a debug APK. It now stages the debug payload, the same Pythonruninstalls.--smoke --releaseis unchanged.
The missing-pyproject.toml hint no longer points at init to create one¶
- Fixed: a verb run without a
pyproject.tomlsuggested runningkivyforge initto create one, butinitdoes not create one: it stops and asks for a[project]table first. The hint now says to write the[project]table, then runkivyforge init -p <platform>.
The dice-roller example no longer names the author's Developer ID¶
- Changed:
examples/desktop/dice-rollerno longer has a[tool.kivy.macos.signing]table, sopackage -p macosad-hoc signs it on any Mac. Before, the table named the author's certificate, and the desktop quickstart told readers to delete it before locking. That step is gone.
A pyproject.toml saved with a byte-order mark loads¶
- Fixed: a
pyproject.tomlthat starts with a UTF-8 byte-order mark failed with "invalid TOML: Invalid statement (at line 1, column 1)". Some editors write one, and so does PowerShell's-Encoding utf8. kivyforge now ignores it. - Adding or removing the mark does not make an existing lock stale.
Pillow is a core dependency; the linux, windows and android extras are gone¶
- Changed: Pillow is now installed with kivyforge. Before, a project that set an icon or splash image for Android, Linux or Windows needed the matching extra, and without it the build stopped partway through.
- Removed: the
linux,windowsandandroidextras. Their only content was Pillow. - Migration: install plain
kivyforge. An old command such aspip install "kivyforge[android]"still works: pip warns that the extra does not exist and installs kivyforge with Pillow.
init: the buildozer.spec key map names real keys¶
- Fixed: when
initfinds abuildozer.specand nopyproject.toml, it prints how each buildozer setting maps onto kivyforge. Several entries pointed at keys kivyforge does not read: android.gradle_dependenciesnow maps to[tool.kivy.android.gradle].dependencies.android.add_jarsnow maps to[tool.kivy.android.native.jars].android.add_activitiesnow maps to[[tool.kivy.android.activities]].android.add_srcis no longer listed as unsupported. It maps to[tool.kivy.android.src].java.- The map now also covers
android.add_aars,android.add_gradle_repositories,android.add_assets,android.add_resources,android.manifest.intent_filters,android.manifest_placeholders,android.allow_backup,android.apptheme,android.numeric_version,android.presplash_colorand the adaptive icon layers. - The closing reference link now points at the user reference,
docs/guides/reference/pyproject/android.md.
iOS: run --device builds for the phone it installs on¶
- Fixed: with automatic signing,
run -p ios --devicecould build and sign successfully, then fail to install with "This provisioning profile cannot be installed on this device". This happened on any phone added after Xcode last created the managed profile. The build targeted any iOS device, so Xcode kept the old profile.run --devicenow chooses the phone before building and passes it toxcodebuildas-destination, so Xcode adds the phone to the managed profile, or stops the build with an error naming the phone. - A missing phone is now reported before the build instead of after it.
build --devicehas no target phone and is unchanged. - Adding a phone to the profile requires an Apple account signed in to Xcode (Xcode > Settings > Accounts). Without one, the build now fails with Xcode's "No Accounts" error instead of producing an app the phone refuses.
iOS: unpinning a provisioning profile takes effect¶
- Fixed: after removing
provisioning_profile(or clearingteam_id), the next build still carried the old value in the generated.xcodeproj, which kivyforge updates in place rather than recreating. Withauto_signing = true, Xcode then failed with "conflicting provisioning settings". This is the fix kivyforge itself suggests when it refuses an Xcode-managed profile. Build settings kivyforge owns are now removed from the project when the configuration no longer sets them. Settings kivyforge does not own, including any set in Xcode's editor, are left alone. - Migration: none. A project left in this state is repaired by the next
build; deleting<app>-ios/is no longer needed.
iOS: run --device finds the phone on Xcode 27¶
- Fixed: with Xcode 27,
run -p ios --devicerefused a single connected phone with "multiple paired iOS devices found", because Xcode 27'sdevicectlalso lists simulators as paired iOS devices.--devicenow ignores simulators, so one connected phone is picked without--destination. - Fixed:
--destinationrejected the UDID thatrun --list-devicesprints. It now accepts the UDID, as well as the name and devicectl's own identifier. --device --destinationmatches only iOS devices, so a name fragment such as your first name no longer picks a paired Apple Watch. A destination that names a simulator now says to use--simulatorinstead of "no device matches".
iOS: a pinned profile Xcode will refuse stops the build up front¶
- Changed: a device or release build,
run --device, andpackagenow stop beforexcodebuildin two cases where Xcode refuses the pinned profile. Before, both built the whole app and then failed (exit 5) with Xcode's error, whiledoctorreported PASS: provisioning_profileset withauto_signing = true(the default). Xcode rejects a pinned profile under automatic signing.- an Xcode-managed profile (
iOS Team Provisioning Profile: ..., the kind automatic signing installs) pinned underauto_signing = false. Manual signing requires a profile created at developer.apple.com. doctor -p iosnow FAILs the Provisioning profile check in both cases.packagenow runs the entitlements check thatbuildalready ran, so an entitlement the pinned profile does not grant fails before the archive instead of at export.KF-ENTITLEMENTS-UNGRANTEDis retired. It warned about ungranted entitlements under automatic signing with a pinned profile, a combination that never built. It is no longer emitted and will not be reused.- Migration: if you pin
provisioning_profile, setauto_signing = falseand pin a manually created profile; or removeprovisioning_profileand let Xcode manage signing. Re-lock afterwards. Simulator builds are unaffected.
iOS: run streams the app's console¶
- Fixed:
run -p ioslaunched the app but never showed its output, and Ctrl+C discarded it; the quickstart said it streamed. The launch now runs attached to your terminal, as macOSrundoes, until the app exits or you press Ctrl+C. On a device the launch passesdevicectl --console, which attaches to the app's output and waits for it to exit.
iOS: --arch offers only the simulator slice kivyforge pins¶
- Fixed:
build -p ios --arch aarch64suggestedx86_64, which then crashed with a traceback (no--jsonenvelope) because no lock pins an x86_64 simulator slice. The error now suggestsarm64only. - On an Intel Mac, a simulator build fails before downloading anything, with
exit 3 (
KF-HOST-INCAPABLE), and says to build for a device or on Apple Silicon. Device builds on an Intel Mac are unaffected. - A wheel the lock does not cover for the requested slice is now a
configuration error with a
--jsonenvelope, and says to re-lock.
macOS and Linux: the built bundle is readable by other users¶
- Fixed: the macOS
.appand the Linux AppDir were created mode0700, so only the user who built them could open them, andcp -Rcarried that into a DMG or/Applications. They now get the mode a plainmkdirwould (0755under the usual umask). Rebuild to pick it up.
CLI: help text matches what each option does¶
- Fixed: the top-level help called kivyforge "a declarative iOS bundler";
it now names all five platforms. Its em dash also printed as
ΓÇöwhen help was piped on Windows; help text is now ASCII-only, and a test keeps it so. openhelp, and its error on platforms without an IDE project, now say it opens Xcode for iOS and Android Studio for Android.--team-id,--signing-identity, and--export-methodnow say which platforms read them.package --signing-identityapplies to iOS and macOS only; Windows signs with[tool.kivy.windows.signing].thumbprint, and Android with--keystore.- Changed:
buildandpackagenow refuse those options, andpackage's--notarize/--no-notarize/--notary-profile, on a platform that does not read them, as they already refused Android-only options elsewhere. Before,package -p windows --signing-identity Xsucceeded and signed nothing with X. The error names each option and the platforms it applies to (exit 1). - Migration: a script that passes one of these to a platform that ignores it, such as one command line shared across targets, now fails; drop the option for that platform. Nothing it did changes: the option was never applied there.
Config: unknown keys under [tool.kivy] are errors¶
- Changed: a key kivyforge does not recognise anywhere under
[tool.kivy]now fails every verb with a configuration error (exit 1). Before, it was dropped without a word and the build went ahead as if it had never been written: a typo such asdeployment_targt, or[tool.kivy.android.proguard], which the Android spec describes but no release implements, so a release build shipped with none of the keep rules it declared. - The error names the key and its line, and suggests the closest valid key
(
did you mean 'deployment_target'?) or lists the valid ones. All unknown keys are reported in one run. - Not checked: keys outside
[tool.kivy]; tables passed through to another tool (info_plist,entitlements,xcode.build_settings, the Androidmanifestattribute tables,gradle_properties); and a table for a platform kivyforge does not support yet, such as[tool.kivy.web], which spec 01 promises is inert. Targeting a misspelt overlay now names it:found [tool.kivy.andriod]; rename it to [tool.kivy.android]. - Migration: a project with an unknown key stops building until the key
is fixed or removed. No setting changes meaning: every key the error names
was already being ignored. Remove
[tool.kivy.android.proguard]if you set it; it never took effect.
Windows: package warns when a bundle only fits in a short folder¶
- Windows limits a path to 259 characters unless long paths are enabled, and they are off by default. Every file in an app has to fit, including the folder the user unzips or installs it into, so the deepest path inside the bundle decides how long that folder can be.
kivyforge package -p windowsnow measures this. When the bundle leaves less than 100 characters for its folder, it prints a warning naming the deepest path and the longest folder that will work, and reportsKF-PATH-DEPTHin--jsonoutput. A typical Kivy app leaves about 154, so most projects will never see it; deep dependency trees can.- Measured, not estimated: with long paths off, a bundle in a folder of exactly the reported length works, and one character longer does not.
- No change to the launcher. Declaring it
longPathAwarewould not help: Windows refuses to start an executable from a folder too long for its own path even with long paths enabled, and the bundledpython.exe, which does the file access, already opts in.
iOS: provisioning_profile works as documented¶
- Fixed:
[tool.kivy.ios.signing].provisioning_profilewas read two incompatible ways. Xcode received it as a profile name or UUID (the documented form), butdoctorand the entitlements pre-flight treated it as a file path. A name or UUID madedoctorFAIL ("not found") and silently skipped the entitlements check; a path passeddoctorand then failed the Xcode build. - A name or UUID is now looked up among the profiles installed on the Mac, so
doctorand the entitlements check use the same profile Xcode signs with. - A path to a
.mobileprovisionis also accepted: kivyforge reads the file and passes its UUID to Xcode.doctorwarns if that profile is not installed, since Xcode signs only with installed profiles. - Migration: none for the documented name/UUID form. If you set a path, it keeps working, and now also reaches Xcode correctly.
Linux: --arch aarch64 is accepted¶
- Fixed:
buildandpackagerejected--arch aarch64as a usage error, although the Linux spec documentspackage -p linux --arch aarch64. Witharchs = ["x86_64", "aarch64"]the Raspberry Pi AppImage could only be produced by reordering the list. The shared--archchoices now includeaarch64; with no--arch, the first locked arch is still the default. runstill does not acceptaarch64: that arch is cross-built only, andrunlaunches the result on the build host.build -p ios --arch aarch64now fails with a clear message instead of deriving a nonexistent iOS platform tag.
Install: pip is now a dependency¶
- Fixed:
uv tool install kivyforgeproduced an install where everykivyforge lockfailed. kivyforge runspython -m pipto resolve and stage wheels, and uv tool environments ship without pip. The failure was also misleading: it reported that a dependency had no wheel upstream. - kivyforge now depends on
pip>=25.1(the Android resolver's floor, which also covers iOS's 24.3), so every installer, including uv and pipx, gets a pip new enough for every target.
Docs: end-user documentation site (MkDocs Material)¶
- Added: a task-based user guide — installation, per-platform quickstarts,
concepts, per-platform how-to guides, a full configuration/CLI reference, and
troubleshooting — built with MkDocs Material and published to GitHub Pages
(roadmap item 6). Sources live in
docs/guides/; the design documents underdocs/design/stay unpublished. - The host matrix, exit codes, and
KF-*diagnostic vocabulary are generated at build time from kivyforge's owncapabilities/reportmodules, and the CLI reference from the click command tree, so the site cannot drift from the tool.mkdocs build --strictruns in CI and fails on a broken internal link, a missing nav page, or aKF-*code the reference does not document.
Desktop: clear error when the runtime can't be staged on a case-insensitive filesystem¶
- Fixed: every python-build-standalone Linux runtime ships 25 pairs of
paths that differ only by case, all in
python/share/terminfo(h/hp70092Abesideh/hp70092a). Building a Linux app onto a case-insensitive filesystem, such as/mnt/cfrom WSL2, used to die partway through staging with a bareshutil.Error. Worse, extracting straight onto such a filesystem silently overwrote one file of each pair. - Staging now checks the runtime archive before extracting it, and stops
with the number of colliding paths, an example pair, the directory, and what
to do instead (under WSL2: build from the Linux side, e.g. under
~). - Applies to the Linux, macOS and Windows runtime stagers. The filesystem is only probed when an archive actually has such collisions; no macOS or Windows runtime measured so far does, so those builds are unaffected.
Desktop: kivyforge run --release (Linux, macOS, Windows)¶
run --releasenow works on the desktop backends, as it has on Android since the entry below. Previouslyrunon Linux, macOS and Windows always built the dev flavor, sobyte_compile/strip_source(both release-only by default) could only be reached throughkivyforge package— never through the command used for day-to-day iteration. That gap is where the LinuxAppRundefect lived: shipped, reproducible in one command, and invisible to every verb a developer runs while working.- The release flavor is built into the same
build/<platform>/treerunalways launches, so a later plainrunrebuilds it with readable source. --release --no-buildis refused:--no-buildlaunches whatever the last build left, and nothing records whether that was a release build.run -p ios --releaseis refused rather than ignored — an iOS release build is an archive and a signed.ipa, not a simulator install. Usekivyforge build --releaseorkivyforge package -p ios.
kivyforge lock honors GH_TOKEN / GITHUB_TOKEN¶
- Fixed: resolving the python-build-standalone runtime queries GitHub's
API, which allows only 60 requests/hour per IP unauthenticated — enough
for a dev box, not for CI, where a shared runner egress address can exhaust
it and fail the lock with
HTTP Error 403: rate limit. A single resolve can spend up to 16 of those requests. lock(andlock --check, which re-resolves) now sendsGH_TOKENorGITHUB_TOKENas a bearer token when either is set, raising the ceiling to 5000/hour.GH_TOKENtakes precedence, matching theghCLI. An empty or whitespace-only value is ignored rather than sent, so an unset CI secret degrades to an anonymous request instead of a 401.- No configuration is needed to keep the old behavior: with neither variable set, the request is anonymous exactly as before.
- On GitHub Actions,
env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}needs no setup. A rate-limited failure now says so in its error message.
Android: manifest passthrough values were escaped twice¶
- Fixed: a value in
[tool.kivy.android.manifest.application]or[tool.kivy.android.manifest.activity]containing&,<or>reached the device with the escape still in it —android:description = "Rock & Roll"shipped as the literal textRock & Roll. The value was escaped on its way into the attribute and then escaped again when the attribute was quoted. Values without those three characters were never affected, which is every in-repo example and most real configs. - Found by the new merged-manifest T3 check (below) on its first run.
Android: two new post-build checks in CI¶
- The
android_gradlejob now asserts that AGP's merged release manifest still contains whatpyproject.tomldeclared — applicationId, version code/name, min/target SDK, every declared permission, feature, service, extra activity and intent-filter, and that no${placeholder}survived unresolved. The release policy pass already read this manifest to lint its posture; nothing checked that the project's own declarations had survived the merge. - The same job now runs
apksigner verifyon the release APK at the project's ownmin_sdk, and checks the v1 (JAR) signing scheme against[tool.kivy.android.signing].v1_signing. - Both are test-only additions; no behavior change for builds that were already correct.
Android: kivyforge run --release¶
run --releasenow builds and launches the release variant, not just--smoke --release. Previouslyrunalways built debug, sobyte_compile/strip_source(both release-only by default) could only ever be exercised throughkivyforge package— never through the command developers actually use for day-to-day iteration.- Skips
package's manifest-policy and lint gates (those belong to the distribution path); signing falls back to the debug keystore when release signing isn't configured, sorun --releasenever requires release secrets to work.
iOS: a Swift Package with a signable target (e.g. Firebase) now builds for device/release¶
- Fixed: an iOS app with a Swift Package dependency that has its own
signable target (e.g. Firebase's resource-bundle targets,
Firebase_FirebaseCore) could fail to build withSigning for "..." requires a development team, even with automatic signing and a team correctly configured — the app's own target built and signed fine, but Xcode builds a Swift Package's targets as a separate, synthesized sub-project that does not inherit signing from either the consuming target's or the project's.pbxprojbuild settings.DEVELOPMENT_TEAMnow also goes out as anxcodebuildcommand-line override (asCODE_SIGN_IDENTITYalready did) for--devicebuilds and--releasearchives, which is what the package's own targets actually read. - An earlier attempt at this fix (also writing
DEVELOPMENT_TEAM/CODE_SIGN_STYLEto the project's own build configuration, matching what Xcode's UI does when you set a Team in Signing & Capabilities) is kept — it's harmless and correct for the app's own target — but was verified, against the realfirebase-ios-sdkpackage, to be insufficient on its own: the identical error still reproduced with only that fix in place. - Reported by an early user after adding the Firebase SPM package; reproduced
and confirmed fixed against the real package (
FirebaseCoreproduct,firebase-ios-sdk11.15.0) on a real device build, 2026-09-21.
iOS: a remote Swift Package no longer embeds by default¶
- Changed:
[tool.kivy.ios.native.swift_packages]'sembedfield now defaults tofalsefor aurl(remote) package; apath(local) package is unchanged and still defaults totrue. A remote package's library product is almost alwaysautomatic, which Xcode resolves to static for a standalone consuming target — there's no.frameworkfor Xcode to copy, so the oldembed = truedefault failed atxcodebuild buildwithThe file "<Product>" couldn't be opened because there is no such filefor most third-party packages, Firebase's included. A localpathshim is author-controlled (its ownPackage.swiftcan deliberately declaretype: .dynamic) and keeps the old default. - Migration: if a remote package's product really is a dynamic framework
(check its own docs — Sentry's
Sentryproduct is one example), setembed = trueexplicitly; it no longer comes for free. - Reported by an early user after adding
FirebaseAuth; reproduced against the real package and confirmedembed = falsefixes it. Seedocs/design/dev/ios-firebase-spm-signing-findings.mdfor the full investigation, including why a reactive fix (matchingxcodebuild's error text) was rejected as fragile in favor of this proactive default.
iOS: a missing SDL3 now fails the build, not just the launch¶
- A Kivy iOS app that stages
SDL3.xcframeworknow fails to compile if its SDL3 headers are somehow missing (a broken vendoring step, a manually editedHEADER_SEARCH_PATHS), with an actionable error naming the cause. Previously this silently compiled the headless (windowless) fallback path instead — an app that launches but can never open a window, with nothing in the build log explaining why. - Non-Kivy (pure-Python) apps are unaffected; the guard only activates when SDL3.xcframework is actually staged.
- Credit: PR #1 (kengoon).
kivyforge init -p is now repeatable¶
- Seed several platforms in one run:
kivyforge init -p ios -p androidwrites both overlays intopyproject.tomlin a single call — the shared[tool.kivy]table is written once, and each platform's own overlay is added or (with--force) regenerated in turn. Previously this took oneinitinvocation per platform. --json's shape is unchanged for the common, single--pcase (platform+data.action/data.tables). With more than one-p, the envelope'splatformisnulland results are listed underdata.resultsas{"platform", "action", "tables"}per platform, in the order given.- Every other verb (
build,run,package,doctor,status, ...) is unaffected — their-pstill takes exactly one value, as it must.
entry_point now runs as __main__ on every platform¶
- Migration note.
if __name__ == "__main__":blocks in your entry module now execute on Android and iOS, matching the behavior Linux, macOS and Windows already had. Previously the entry module was only imported on mobile, so that idiom silently never ran there — no error, nothing in the log. If your app already callsApp().run()unconditionally at module top level (the style every in-repo example uses), nothing changes for you. - Breaking for one specific pattern: an
entry_pointpointing at a package (not a module) now requires a__main__.pyon every platform, same aspython -m mypackage. Before this change, Android and iOS ran the package's__init__.pyinstead. Add a__main__.py, or pointentry_pointat an explicitly-named module instead (e.g.entry_point = "myapp.start"). - Also changes: the entry module is no longer left in
sys.modulesafter startup (mobile only — desktop already worked this way). Code that expects toimportits own entry module again by name after startup will now re-execute it instead of getting the cached one. - Implementation: Android's
main.cand iOS'skivyforge_bootstrap.mboth now run the entry module viarunpy.run_module(entry_point, run_name="__main__", alter_sys=True). Credit to PR #1 (kengoon), which independently found and fixed the iOS half of this first.
Desktop apps start faster: the embedded stdlib ships compiled¶
- The staged Python stdlib is byte-compiled at build time on Windows, Linux
and macOS. Measured on a packaged
dice-roller:import kivycosts 66 ms instead of 276 ms — about 210 ms off every launch. Bundles previously shipped the stdlib as source and re-parsed it on each start, which neither the.AppImage(read-only) nor the signed.app(writing to itself invalidated its signature) could ever cache away. - Stdlib sources are kept, unlike the app payload: they are what tracebacks,
inspectandlinecacheread.strip_sourcestill applies only to your app and its site-packages. - Follows the existing
[tool.kivy.<platform>.build_settings].byte_compilesetting, so it applies topackageby default and to every build when that istrue. No new option. - The byte-compile subprocess no longer seeds the artifact with an arbitrary handful of its own import caches, so that subtree is reproducible.
Spawned tools can no longer wait for input¶
- Every external tool kivyforge runs is given a closed stdin. A child that unexpectedly prompts now gets EOF and fails fast, instead of blocking until a CI job times out with nothing explaining why.
- pip is passed
--no-inputat the lock resolvers and the wheel staging installs: an index that wants credentials is the one case here that genuinely prompts. kivyforge runis deliberately unchanged — the app it launches is your program and keeps the user's stdin.
Linux aarch64 is a Raspberry Pi target¶
[tool.kivy.linux].archsaccepts"aarch64"(opt-in; the default staysx86_64). Cross-build on a Linux x86_64 host; run the AppImage on a 64-bit Raspberry Pi OS image (Pi 4 or 5). kivyforge never runs on the Pi. Verified 2026-09-17 on a Pi 5 (Debian 13, labwc/Wayland, Broadcom V3D); a Pi 4 has not been run.appimagetoolis the host-arch binary; the aarch64 type2 runtime is embedded with--runtime-file. 32-bit ARM and musl stay out of scope.
New: kivyforge capabilities¶
- A verb that answers what this kivyforge can do, with no project, lock or
network needed — so an agent can ask before it has anything to point at.
--jsonpublishes: platforms[]: each target'sarchs,package_formats,default_package_format, aliases, the host it is the default target for, andhosts: which of macOS/Linux/Windows can build it at all. That last one is the fact that stops an agent attempting iOS on Windows, and nothing in--helpstates it.verbs[]: every registered verb and whether it accepts--json.exit_codesanddiagnostic_codes: the reserved exit taxonomy and the publishedKF-*vocabulary.- Everything is derived rather than restated: the host matrix comes from asking
each backend's own
check_host_capability, the verb list from the click group, the codes from the modules that define them. A hand-kept copy would pass its tests right up until it went stale.
--json on clean, init, upgrade and open¶
- Every verb but
runnow takes--jsonand--no-color. Each reports what it did rather than that it did something: clean:data.removedlists the generated trees actually deleted (relative, posix), pluscache_flushedandgradle_caches. Recorded as they go, so a removal that fails part way still names what it had removed.init:data.action(added/regenerated),data.tables— the[tool.kivy*]tables written — anddata.pyproject.upgrade:data.refreshednames each re-fetched artifact anddata.skippedcounts vendored (path-pinned) entries. A hash mismatch half way through still reports the artifacts already refreshed.open:data.projectis the project that was opened.- Two new diagnostic codes, both warnings on an
ok: truerun:KF-IDE-NOT-FOUNDwhenopenfinds no Android Studio launcher on PATH (the project is still generated, just not launched), andKF-DEPENDENCY-DRIFTwheninitsees an installed dependency outside its declared specifier. open -p iosnow prints the project it opened, matching what Android has always printed. It used to succeed silently.- As with
build/package, progress moves to stderr on these verbs:clean's "Stopped the project's Gradle daemon(s)." andupgrade's per-artifact "Refreshing …" lines. What each verb produced stays on stdout.
Breaking: build and package progress moves to stderr¶
- For
buildandpackage, everything except the product lines now goes to stderr, on all five platforms: staging and signing progress, Android's[collect]/[stage]/[gradle]/[policy]lines, and Gradle's own output (also for Androidrunand--smoke). stdout keeps only what was produced (Built …,Generated …,Packaged …,Exported …) and the distribution advice. On a terminal nothing looks different: the text and its order are unchanged. - Migration: a job that captures only stdout
(
kivyforge package -p android > build.log) no longer gets the build log. Capture both streams (> build.log 2>&1). - When
xcodebuildorappimagetoolfails, its output is printed as progress and the error is a one-line summary (xcodebuild archive failed (exit 65); its output is above.) instead of embedding the whole log.appimagetoolfailures now show both of its streams; stdout used to be dropped whenever stderr had anything in it.
build --json and package --json¶
- Both verbs take
--jsonand--no-color, like the other verbs. The envelope'sdata.artifactslists what this run produced, as{"path", "kind"}with the path relative to the project root andkindone ofappimage,apk,aab,app,ipa,folder,project. - Only products this invocation finalised are listed, including on failure: a
buildthat fails after generating the project still names the project; apackagethat fails names nothing, never a previous run's artifact. - Success-path warnings arrive as diagnostics on an
ok: trueenvelope:KF-SIGNING-UNCONFIGURED,KF-BYTECOMPILE-NO-INTERP,KF-MANIFEST-POLICYandKF-ENTITLEMENTS-UNGRANTED. - Failures now say what kind of failure they are.
buildandpackageused to exit1for everything; they now use the reserved exit codes, with a matching diagnostic code:
| Exit | Code | When |
|---|---|---|
3 |
KF-HOST-INCAPABLE |
wrong host OS for the target |
3 |
KF-TOOLCHAIN-MISSING |
a tool is not installed (xcodebuild, the Gradle wrapper, clang, codesign, signtool, appimagetool, …) |
3 |
KF-TOOLCHAIN-UNUSABLE |
a tool is present but cannot be executed; context.errno says why |
4 |
KF-LOCK-MISSING / KF-LOCK-UNREADABLE / KF-LOCK-DRIFT |
re-run kivyforge lock |
5 |
KF-BUILD-TOOL-FAILED |
Gradle, xcodebuild or appimagetool exited non-zero; context has tool and task |
5 |
KF-ARTIFACT-MISSING |
the tool reported success but the product is not there |
Anything not listed keeps KF-ERROR and exit 1. A tool that cannot be
started at all used to crash with a traceback on iOS and Android; it now
fails cleanly with an envelope.
- Android now prints Generated <app>-android and relative artifact paths, like
the other platforms. iOS build --simulator/--device builds into the
project's own build/DerivedData (as run already did) and prints the
.app it produced.
Breaking: Apple targets are Apple Silicon only¶
x86_64removed from macOS and the iOS Simulator, as a host and as a target. macOS 27 stops installing on Intel hardware, GitHub'smacos-*-intelrunners sunset in 2027, and macOS 28 removes Rosetta — so an Intel slice is one nothing can execute or validate. Legacy tools continue to cover Intel; a new toolchain does not need to carry it.[tool.kivy.macos].archsaccepts only["arm64"](the new default), and[tool.kivy.ios].simulator_archsonly["arm64"]. Both rejectx86_64with a migration message naming the fix.- universal2 builds are gone along with the
lipomerge machinery, and--arch universal2is no longer a CLI choice. - iOS locks now pin two slices (device arm64 + simulator arm64) instead of three.
- Migration: set
archs = ["arm64"]/simulator_archs = ["arm64"](or drop the keys — arm64 is the default) and re-lock. universal2wheels are unaffected and still resolve normally — a universal2 wheel contains arm64. The build target set narrowed; the wheel tag set did not.
Desktop source stripping (Windows/Linux/macOS)¶
- New
[tool.kivy.<platform>.build_settings]table on Windows, Linux, and macOS, matching what Android's ownbuild_settingsalready does:byte_compileandstrip_source(each"release"|true|false, default"release")."release"applies only tokivyforge package—build/runalways keep readable.pyfor fast iteration. - Only the app sources and installed site-packages are compiled/stripped;
the staged Python stdlib is never touched (readable tracebacks,
inspect/linecachestay intact). - The compiler is the bundle's own staged interpreter when it can run on
the host (never Rosetta or any other emulation), else the interpreter
running
kivyforgeitself on a matching CPython minor, else the default degrades to shipping source with a note (byte_compile = truefails the build instead of silently shipping source). - On macOS this runs before ad-hoc signing, since codesign seals the bundle.
- Previously only PyInstaller (bytecode-only) and Flet (bytecode-only by
default since v0.86) did this; kivyforge's desktop backends shipped raw
.pyunconditionally until now.
iOS source stripping¶
- New
[tool.kivy.ios.python.build_settings]table (nested underpython, since[tool.kivy.ios].build_settingsalready names the Xcode passthrough table): the samebyte_compile/strip_sourcerelease tri-state as Android and the desktop backends, covering bothpip-deps(third-party wheels) and the app's own source. build/runare unaffected —app/stays a symlink toapp_dirfor fast iteration, exactly as before.kivyforge packagenow stagesapp/as a real, disposable copy instead of a symlink and compiles/strips that — the user's actual source tree is never touched. Switching back tobuild/runafterward reclaims the copy for the symlink automatically.- The staged
Python.xcframeworkstdlib is never touched (it's a prebuilt, opaque library either way). Python.xcframeworkships no standalone interpreter to shell out to, so compilation always uses thekivyforge-hosting interpreter itself (on a matching CPython minor) or degrades — there is no "staged interpreter" rung the way desktop has.
Android backend (.apk / .aab)¶
- New
androidtarget.kivyforge lock/build/run/package -p androidresolves intopylock.android.toml, assembles the official python.org Android runtime (per ABI) plus per-ABIandroid_*wheels into a generated Gradle/AGP project, and produces a signed.apk(sideload/CI) or.aab(Play upload). A wheel-assembly + Gradle-drive backend — no python-for-android recipe system, no per-app C-extension compilation. Builds on Windows, macOS, or Linux (consumes prebuilt wheels + runtime; drives the SDK/NDK/Gradle). 64-bit only (arm64_v8a,x86_64— the python.org runtime ships no 32-bit build). [tool.kivy.android]overlay —package(applicationId),version_code(explicit or"auto"derived from[project].version),min_sdk/target_sdk/compile_sdk,kivy_generation(2 = Kivy 2.3.1, 3 = Kivy 3.0),abis, adaptiveicons/splash(AndroidX SplashScreen),permissions(+ implied<uses-feature>synthesis),services/activities/intent_filters, the manifest passthrough (+ raw-XML),native.aars/.jarsand Mavengradledependencies,include_files,src(Java/Kotlin),signing(v1–v4 schemes), andbuild_settings(byte-compile/strip/R8/debug-symbols). Loader-validated (rules 1–21).- kivyforge-owned bootstrap — a generated
org.kivy.android.PythonActivity(extends stock SDL2SDLActivity), the pyjniusNativeInvocationHandlermatched pair (with a hardinvoke0contract gate), the Kivy-compat shims (org.renpy.android.Hardware+mActivity), anSDL_mainnative launcher (NDK-compiled per ABI), and asys.meta_pathfinder over flattened extension.sos injniLibs/. pyjnius ships as a prebuilt Java-free wheel (SDL- agnostic,JNIEnvresolved at runtime). - Four artifact channels —
android_*wheels (PyPI / supplemental indexes / vendoredfind_links), the python.org runtime,.aar/.jararchives, and Maven coordinates (Gradle resolves; the graph is SHA-256-recorded in the lock). - Mandatory signing — debug builds use the auto-managed debug keystore;
packagerequires a release keystore (env-var passwords, never inpyproject.toml), with a signing pre-flight and a release manifest-policy pre-flight (exported components,debuggable, placeholder package, …) that fail before Gradle. kivyforge run --smoke— a generated instrumented contract test validates the extension-module finder + the pyjniusinvoke0glue on a real device/emulator; the promotion mechanism for the compatibility matrix.kivyforge doctor -p android— JDK/SDK/build-tools/NDK/emulator/adb, plus project checks (SDL↔Kivy, the pyjnius contract, a hermetic 16 KB ELF-alignment scanner, ABI coverage, signing, lock-host reachability).- Examples —
hello-android(Kivy 2.3.1 / SDL2),hello-sdl3(Kivy 3.0 / SDL3),pyjnius-deviceinfo(device info via pyjnius), andqr-maven(Google ZXing via the Maven channel + a Python-implements-Javainvoke0round-trip), plusexamples/verify-android.ps1. - First-party wheels, no vendoring. Kivy (2.3.1 and 3.0), pyjnius, and the
matching SDL family are resolved from the
kivy-mobile-wheels
index — a companion repo of first-party cibuildwheel cross-builds (official
SDL sources, 16 KB page-aligned, sonames unchanged for
System.loadLibrary) published as a static PEP 503 index.hello-androidandhello-sdl3carry their locks committed as validation evidence; the rest resolve like any other dependency. - SDL3 / Kivy 3.0 support.
kivy_generation = 3renders the SDL3 bootstrap template (templates/sdl3/) and pairs it with the matching wheel-repo glue;check_sdl_glue_contract()fails the build on any Java/.soversion mismatch rather than the silent black-screen failure that mismatch would otherwise produce. - Validated on real hardware.
kivyforge run --smokepasses on an x86_64 emulator (API 31) and a Pixel 8a (Android 16 / API 36) for both the SDL2 and SDL3 stacks, with the app visually confirmed rendering on-device — see the compatibility matrix.
Windows backend (onedir folder)¶
- New
windowstarget.kivyforge lock/build/run/package -p windowsresolves intopylock.windows.toml, bundles a relocatable python-build-standalone MSVC CPython pluswin_amd64wheels, and produces a run-from-folderonedir: a windowed-subsystem launcher.exebeside the full runtime prefix, the app source, a generated bootstrap, and the native-binaries channel. amd64 only this phase; the config stays list-shaped for additivewin-arm64later. Requires a Windows host (MSVC runtime + Windows-only rcedit/signtool). [tool.kivy.windows]overlay —app_id(the AppUserModelID; hard constraints enforced at config time, PascalCase/period style a doctor WARN),archs,python.version,icons.source, optional[signing](thumbprint/timestamp_url/store_scope), and the sharedextra_index_urls/find_links/exclude.- Prebuilt, reproducible launcher — a single vendored windowed launcher
.exe(built once fromlauncher.cwith MSVC/Brepro+/RELEASE+/EMITTOOLVERSIONINFO:NO, SHA-256 pinned, byte-reproducibility gated in CI — the last flag drops the PE "rich header", whose servicedcl/linkbuild stamp otherwise differs between runner images sharing oneVCTOOLSVERSION) is parameterized per app only by its resources (icon + version) via the vendoredrcedit— no per-build compiler. The launcher self-locates, sets the AppUserModelID, hands the console off onrunwhile suppressing the flash on double-click, and spawns the bundledpython.exeunder a Job object. - No-pip wheel staging — locked wheels are unpacked into the runtime prefix
by a deterministic wheel-scheme installer (routing
purelib/platlib/data/scripts/headers), andkivy_deps.*DLLs land where the loader finds them. Thevcruntime140*.dllare staged from the PBS prefix (empiricallymsvcp140is handled best-effort). - Native-binaries channel + PE checks — the shared staging helper gained
Windows-facing guards (case-insensitive collision keys, reserved DOS
device-name / alternate-data-stream rejection, exec-bit no-op) and a
pure-Python PE machine-type reader so a wrong-architecture DLL fails the build
clearly. The generated bootstrap registers
bin\withos.add_dll_directoryand prepends it toPATH(helpers run by name, DLLs load by name). - Unsigned folder packaging —
package -f folderatomically copies the onedir intodist\windows\<app>-<ver>-amd64\, excluding build cache / VCS / editor droppings; the build tree stays the unsigned dev-run target. - Optional Authenticode signing — first-class but off by default. A new
Signerprotocol withSigntoolSigner(signtool sign /sha1 <thumbprint> /fd SHA256 /tr <url> /td SHA256,/smforstore_scope=machine, always timestamped) andNullSigner; identity is always a cert-store thumbprint, never a.pfx. A deterministic PEP 440 → four-part numericFILEVERSIONmapping feeds the version resource. Signing targets thedistcopy only, after the resource patch. kivyforge doctor -p windows— Windows host, long-path support,app_idstyle, arch coverage, app icon, native-binary sources/collisions/arch, resource assets, reachable hosts (incl. the signing timestamp server),signtoolavailability, and the signing certificate (in the configuredstore_scopestore).- Examples —
desktop-viewer,dice-roller,notes, andhello-nativegained[tool.kivy.windows]overlays (hello-native adds agreet.dll+roll.exenative channel);examples/verify-windows-examples.ps1mirrors the desktop verifier's clean→lock→doctor→build→run→package→launch loop. - CI —
windows-latestunit tests, a launcher reproducibility + vendored-asset gate, and a self-signedsigntoolsign→verify loop.
Linux backend (AppImage / AppDir)¶
- New
linuxtarget.kivyforge lock/build/run/package -p linuxresolves intopylock.linux.toml, bundles a relocatable python-build-standalone gnu/glibc CPython plus manylinux wheels, and produces an AppImage (thepackagedefault) with a run-from-folder AppDir as the substrate/fallback (package -f folder). x86_64 only this phase; the config stays list-shaped for additiveaarch64later. [tool.kivy.linux]overlay —app_id(reverse-DNS; names the generated.desktop,Icon=, andStartupWMClass),archs, optionalglibc_floor,python.version,icons.source,desktop.categories, and the sharedextra_index_urls/find_links/exclude. Invalidapp_idcharacters are a fail-fast config error.- manylinux resolution — because pip does not expand the manylinux hierarchy
from a single
--platform, the resolver requests the full compatible tag ladder at/below the glibc floor in one invocation. The artifact's effective glibc floor =max(runtime floor 2.17, highest locked wheel manylinux level). - AppImage packaging — a pinned static
appimagetool+ pinned type2 static-FUSE runtime are fetched through the shared artifact cache; the shipped AppImage needs no hostlibfuse2, andAPPIMAGE_EXTRACT_AND_RUN=1means the build host needs no FUSE either. A generatedAppRun(POSIX shell) setsPYTHONHOME/PYTHONPATH/SDL_VIDEO_*_WMCLASS— no compiled launcher, no native build at package time. - Generated desktop integration —
.desktopentry + hicolor icon set frompyproject.toml(Pillow for resize via the optionalkivyforge[linux]extra; a default icon is generated with only the stdlib when no source is set). kivyforge doctor -p linux— host libc (WARN on musl),libGL/libEGLvialdconfig, display session, glibc floor vs. runtime, manylinux arch coverage, app icon, generated.desktopvalidity (desktop-file-validate), and reachable hosts (lock + appimagetool/runtime assets).- Host contract (documented, never vendored) — the bundle relies on the host
for glibc ≥ the effective floor,
libGL/libEGL, and an X11/Wayland session. - Examples —
desktop-viewer,dice-roller, andnotesgained[tool.kivy.linux]overlays + committedpylock.linux.toml;examples/run-examples.sh -p linuxbuilds and runs them. - CI — added an
ubuntu-latestheadless job (the cross-platform core now runs on both macOS and Linux hosts). Artifact cache respects$XDG_CACHE_HOMEon Linux (~/.cache/kivyforge).
v3.0.0¶
Complete rewrite. kivyforge is the successor to kivy-ios, python-for-android,
and buildozer — a declarative, PEP 621-aligned build toolchain for Kivy apps
aimed at every platform Kivy runs on. This first release implements the iOS
target, built around pre-built Python.xcframeworks and pip-installable iOS wheels.
The recipe-based kivyforge build-recipe workflow is gone; see the
design docs and the updated README.
What's new in 3.0¶
- Declarative config — project settings live in
[tool.kivy]/[tool.kivy.ios]sections of your app'spyproject.toml; no moretoolchain.specorrequirements.txt. Config validation is strict and fail-fast:[tool.kivy.ios.python].versionis required (no hidden default at lock time), native xcframeworksourcepaths may not escape the project, signing flags must be real booleans (a quoted"false"is rejected, not coerced totrue), and[tool.kivy.ios.xcode.build_settings]values must be strings. - Dependency locking —
kivyforge lockresolves pure-Python and iOS wheel dependencies and writespylock.ios.toml(PEP 751-inspired). - Python.xcframework — Python is distributed as a pre-built xcframework; no source compilation required on the developer machine.
- iOS wheels — third-party packages are distributed as pre-built
*-ios.whlwheels;kivyforge builddownloads and integrates them. - Xcode project generation —
kivyforge initgenerates a complete.xcodeprojviapbxproj;kivyforge openlaunches Xcode. - SDL3 — Kivy 3.0 moves to SDL3; the generated project and Info.plist are configured for the SDL3 UIScene lifecycle out of the box.
- Mobile window/display geometry via
kivy.mobile— DPI, scale, safe-area insets, and keyboard height are provided by Kivy core'skivy.mobilemodule (kivy/kivy#9331), shipped in the Kivy iOS wheel. - Native iOS dependencies — declare Swift Package Manager packages and
native
.xcframeworkarchives under[tool.kivy.ios.native].kivyforge lockpins SPM packages to a concrete revision and resolves each xcframework archive (by URL or repo-relative path) into a SHA-256 + enumerated slice list inpylock.ios.toml;kivyforge buildfetches, verifies, and wires them into the Xcode project's Link / Embed phases per each entry'slink/embedintent, pruning references for frameworks no longer present. When two sources stage the same<name>.xcframework, identical content (matching tree hash) is deduplicated silently and conflicting content fails the build, naming both providers and their hashes. - Slim CLI —
toolchainnow dispatches toinit,lock,build,run,open,upgrade,clean,status, anddoctor; legacy 2.x verbs print a migration pointer. - Requires Python ≥ 3.13 and pip ≥ 24.3 on the developer machine (macOS
only). pip 24.3 added PEP 730 iOS platform-tag matching, which
toolchain lockrelies on to resolve iOS wheels;kivyforge doctorflags older pip andkivyforge lockfails fast with an upgrade hint.
Historical entries below this line describe kivy-ios 2.x (the recipe-based toolchain). They are preserved for reference but describe a workflow that no longer exists in 3.0.
v2025.05.17¶
Closed issues:
- kivy fails to build on MacOS 15.4.1 (-armv7 compiler failure) #965
- push notification in ios? #964
- Libffi error while building python3 with Xcode 16.3 #963
- Fails to build on recipe for ios/src/ios.pyx #948
- target python needs to be patched #939
- Toolchain build kivy command is failing with Xcode 16 #936
- Toolchain does not build with Xcode 15.4 #929
- How is Accelerate not supported? #925
- M1 mac python3 build error #924
- Python error after building to iPhone Simulator #923
- Pillow not building #907
- Can't create a recipe #906
- Linker Command Failure Error While Building Python3 #905
- MacOS kivyforge build constantly failing (I think due to C extension errors) #901
- Unable to build with kivymd==2.0 #897
- Got dlopen error on Foundation #896
- Feature request: MatplotLib for iOS #712
Merged pull requests:
- Use
-platform_versionflag during linking instead of the deprecated and now removed)-ios_simulator_version_min#970 (misl6) - Bump
libpngversion to1.6.48#967 (misl6) - Disable iOS armv7 build for
libffi#966 (misl6) - Add
SDL3,SDL3_ttf,SDL3_mixer,SDL3_imagerecipes for Kivy3.0.0#961 (misl6) - chore(deps): update pypa/gh-action-pypi-publish action to v1.12.4 #958 (renovate[bot])
- Enhance Cython integration: Add support for
c++mode, use hostpython3-provided Cython, allow pinning of specific Cython version in recipe #957 (misl6) - Bump Kivy version to 2.3.1 #956 (misl6)
- chore(deps): update pypa/gh-action-pypi-publish action to v1.12.3 #952 (renovate[bot])
- chore(deps): update pypa/gh-action-pypi-publish action to v1.12.2 #942 (renovate[bot])
- Fix libb2 issue #940 (tcaduser)
- :construction_worker: Fix broken setup workflow #938 (AndreMiras)
- fix build for Xcode 16 #937 (tcaduser)
- Fix zbarlight recipe #932 (Cheaterman)
- chore(deps): update actions/setup-python action to v5 #918 (renovate[bot])
- chore(deps): update pypa/gh-action-pypi-publish action to v1.10.2 #917 (renovate[bot])
- Bump minimum Python supported version to
3.8#914 (misl6) - chore: Configure Renovate #910 (renovate[bot])
- Sunset
apple-silicon-m1self-hosted runner, as now is supported by Github Hosted runners viamacos-latesttag. Usemacos-13for runs on Intel macs #909 (misl6) - (
recipes/materialyoucolor): update to 2.0.9 #904 (T-Dynamos)
v2024.03.17¶
Fixed bugs:
- zbarlite recipe has hardcoded python version #677
Closed issues:
- Materialyoucolor-python Recipe #895
- libffi recipe build hangs during configuration #889
- Can't Input text on IOS17.2 Simulator with Xcode 15.1 #888
- [Request]: Pypi packages #886
- Add pdf2docx recipe #883
- [Question] : Pygame and pyexpat ? #873
- Support mail address is broken #872
- Toolchain python version mismatch #869
- how to show ip camera footage #868
- Working app crash on iOS 17 #864
- Crash during background execution #862
- Python3 build fails with
unknown argument: '-fp-model'#860 - issue creating recipe for kiwsolver #857
- App runs fine from XCode, but terminates with an error if installed through TestFlight #850
- ffmpeg related errors after adding this recipe to the project #849
- how do you init your custom c module at app startup? #848
- Replacement for pycrypto #843
- Xcode build fails with ModuleNotFoundError: No module named 'encodings' #842
- Invalid Signature when adding Python to iOS App Extension #826
- APP crashes easily #824
- when creating xcodeproj using kivy-ios, how to change the default bundleIdentifier #823
- kivyforge build: no matching architecture in universal wrapper #792
- Explanation to build recipe in iOS #776
- Support arm64 simulator on Apple Silicon hardware #751
- PyObjC import error #741
- App Store Connect Operation Error Invalid Bundle Structure - The binary file '*myapp*.app/lib/python3.9/site-packages/google/protobuf/pyext/_message.cpython-39-darwin.so' is not permitted. #702
- libzbar recipe missing libiconv dependency #676
- Lib Not Found #674
- kivyforge build kivy Error #668
- leverage conda to create the host* packages #655
- ImportError: dynamic module does not define module export function (PyInit__imaging) #644
- kivy ios and boto3 #641
- Recipe for OpenCV #637
- Issues with using threads on an iOS device #609
- Xcode fails to build: Undefined symbols for architecture x86_64 #607
- There is a memory leak in the program #419
- Include Unit tests and CI to test build for the toolchain #296
Merged pull requests:
- :construction_worker: Fix the rebuild recipe script #900 (AndreMiras)
- recipes: add new
materialyoucolorrecipe #898 (T-Dynamos) - Remove bitcode support, as is now deprecated by Apple #894 (misl6)
- Bump openssl version to
1.1.1w#891 (misl6) - Bump Kivy version to 2.3.0 #890 (misl6)
- Set
fetch-depth: 0for updated recipes checkout and useactions/checkout@v4instead ofactions/checkout@v3#884 (misl6) - Update sdl2 deps to reflect the same targeted in kivy/kivy #881 (misl6)
- Make doc structure consistent and up-to-date #879 (Julian-O)
- Create no-response #878 (Julian-O)
- Support Section Updated issue #872 resolved #876 (seharbat00l)
- Install kivy-ios via pip during tests, so dependencies are automatically managed #871 (misl6)
- Update
python3recipe to3.11.6#870 (misl6) - Do not use full path for system libraries and frameworks in
.xcodeproj#867 (misl6) - Fix for upstream issues with compiler path #866 (tcaduser)
- update to matplotlib 3.7.2 #861 (tcaduser)
- update for recent iOS platform changes #859 (tcaduser)
- matplotlib recipe #858 (tcaduser)
- Bump
numpyversion to1.24.4#856 (misl6) - Support ARM64 Simulator + Introduce build platform concept + Introduce
xcframework#778 (misl6)
v2023.08.24¶
Fixed bugs:
- ld: 407 duplicate symbols for architecture arm64 #787
Closed issues:
- please delete #846
- Label and MDLabel don't work correctly on iOS #845
- ImportError: dynamic module does not define module export function (PyInit__bcrypt) #841
- Build failure #838
- Custom recipes just like p4a #837
- pandas recipe #836
- Build numpy: '['/kivy/dist/hostpython3/bin/python', '-m', 'cython', '-3', '--fast-fail', '-o', '_philox.c', '_philox.pyx']' returned non-zero exit status 1.' #835
- ios.c not found when building ios for kivy #833
- Imvaild symlink #825
- dynamic module does not define module export function (PyInit__multiarray_umath) #822
- Toolchain build openssl not working #819
- OSError: codec configuration error when reading image file #818
- Command PhaseScriptExecution failed with a nonzero exit code #817
- We discovered that the content didn’t load after launched. #816
- Asset validation failed Invalid bundle structure. The “soccer.app/lib/python3.10/site-packages/charset_normalizer/md.cpython-310-darwin.so” binary file is not permitted. #815
- Asset validation failed Invalid Bundle Structure for libnpyrandom.a and libnpymath.a #814
- kivy-ios Not found '_SHA256.so' #813
- The toggling of the 'readonly' attribute (True/False) for the MDTextField is not functioning as expected in the xcode build #811
- libpng recipe won't build #808
- build error in Python3.8 #796
Merged pull requests:
- Use a pinned version of
Cythonfor now, as most of the recipes are incompatible withCython==3.x.x#844 (misl6) - Remove Unused Dependency:
requests#840 (gdrosos) - Fix download from
sourceforge(and possibly many others) #830 (misl6) - Now Github Actions provides
python3viasetup-pythonalso for Apple Silicon macs #829 (misl6) - Bump Kivy version to 2.2.1 #828 (misl6)
- Update
libpngrecipe and buildSDL2_ttfvendoredfreetypewith png support, so can render colored emoji #827 (misl6) - SDL_ttf: Hide internal symbols via single-object prelink #820 (misl6)
- Update zbarlight recipe to 3.0, refactor #805 (Cheaterman)
- Refactor biglink ; add extra assertions to help debug failures #804 (Cheaterman)
v2023.05.21¶
Closed issues:
- Issue adding Pillow to Kivy-ios build #807
- I have created and app using kivy. it works in python through VS code but when I run through Xcode it gets no module named "application name" in main.py, line number 4 error and close the app in simulator. I tried all the ways in previous discussions but still not working. is there any other way I can try? #800
- Kivy ios save an screen as an image to the users ios photos #799
- Command PhaseScriptExecution failed with a nonzero exit code #793
- ImportError: PyInit_interval #788
- Recipe for the cryptography module #786
- Pyaudio #785
- Check #783 #784
- Command PhaseScriptExecution failed with a nonzero exit code #783
- Cocoapods import not found when kivy is installed #777
- sh.CommandNotFound: /Users/suhaan/Documents/weatherappkivy/dist/hostpython3/bin/pip3 #775
- check #773 issue #774
- Asset validation failed Invalid bundle structure. The “theweatherreporterapp.app/lib/python3.9/site-packages/charset_normalizer/md__mypyc.cpython-39-darwin.so” binary file is not permitted. Your app cannot contain standalone executables or libraries, other than a valid CFBundleExecutable of supported bundles. For details, visit: https://developer.apple.com/documentation/bundleresources/placing_content_in_a_bundle (ID: eb2a7a62-515c-459b-a222-5330e609551d) #773
- dynamic module does not define module export function (PyInit_md) error #772
- Toolchain 407 duplicate error of arm64 #771
- bash: toolchain: command not found #770
- when running kivyforge build python3 kivy pillow with arch arm64 with apple sicion getting list index out of range #769
- Error compiling SDL_Image #763
- ImportError: dynamic module does not define module export function (PyInit_PIL__imaging) #711
- Build python3 with mmap module #659
Merged pull requests:
- Fix AnyIO (and asks) support #803 (Cheaterman)
- Update Kivy recipe for 2.2.0 #802 (misl6)
- Use
--no-depsand--platformduring install of recipe python deps #801 (misl6) - Fixes crash on ios.get_safe_area() in iOS 16 #790 (tito)
- Update bridge.m/h to make plyer works with severals implementation #789 (tito)
- Bump
numpyversion to1.24.2( and fix the build on Python 3.10 ✅ ) #780 (misl6) - Disable SDL2 hidapi in order to avoid dependency on
CoreBluetooth#779 (misl6) - 🎉 Python 3.10 support #768 (misl6)
- Upgrade more GitHub Actions plus Python 3.11.1 and Cython 0.29.33 #767 (cclauss)
- Upgrade GitHub Actions #765 (cclauss)
- Update required Xcode version to 13 #764 (misl6)
- Update ffmpeg/ffpyplayer versions #716 (tito)
v2023.01.29¶
Closed issues:
- Unknown class
<BoxShadow>#758 - toolchain pip3 install pycryptodome error #755
- Stuck between two error codes: ImportError...(PyInit__imaging) and 407 duplicate symbols #752
- Toolchain Build error - commandnotfound python #748
- No module named 'xxx' #744
- dynamic module does not define module export function (PyInit__sqlite3) #743
- [BUG] Kivy Ios on mac os monterey arm64 simulator #740
- PhaseScriptExecution failed when hitting Play in Xcode #739
- toolchain.py build python3 openssl kivy not completing after upgrading macOS Monterey 12.5 #738
- Proposal of versioning numbering method change for the next release #729
- xcode archive failure- missing signing identifier ... site-packages/google/..._api_implementation.cpython-39-darwin.so #697
Merged pull requests:
- Update pbxproj pinned version in requirements.txt #757 (misl6)
- Add default Launch Screen storyboard #756 (misl6)
- Flake8 does not support inline comments for any of the keys #754 (misl6)
- Use Python 3.10 for tests #750 (misl6)
- Update SDL2, SDL2_ttf, SDL2_mixer, SDL2_image to latest releases #749 (misl6)
- :rotating_light: Minor linting fixes/updates #747 (AndreMiras)
- Fixes some E275 - assert is a keyword #746 (misl6)
- Use
sys.executableinstead ofsh.python#745 (misl6) - Multiple fixes to README #737 (rshah713)
- Fix typo in README #736 (rshah713)
- Disable configuration of i386-Simulator for libffi #761 (misl6
[2022.07.19] - 2022-07-19¶
Added¶
- Added py3dns recipe #728 (Neizvestnyj)
- Ignore egg-info in git #718 (tito)
- Bump Kivy recipe to a commit that includes latest camera API enhancements #700 (RobertFlatt)
- Add support-requests v2 #689 (misl6)
Fixed¶
- Updated shebangs to target python3. Bumped libffi version to 3.4.2 #688 (misl6)
- Fixes libffi 3.4.2 not linking correctly (missing symbols) on iOS Simulator #695 (misl6)
- Update dpi for latest iPhone models #707 (akshayaurora)
- Our self-hosted Apple Silicon runner now has been migrated to actions/runner v2.292.0 which now supports arm64 natively #710 (misl6)
- Update xcassets to include mandatory icons in latest xcode #717 (tito)
- Bump cookiecutter from 1.7.2 to 2.1.1 #714 (dependabot)
- Use shutil.which instead of sh.which #735 (misl6)
[2022.07.19] - 2022-07-19¶
Added¶
- Added py3dns recipe #728 (Neizvestnyj)
- Ignore egg-info in git #718 (tito)
- Bump Kivy recipe to a commit that includes latest camera API enhancements #700 (RobertFlatt)
- Add support-requests v2 #689 (misl6)
Fixed¶
- Updated shebangs to target python3. Bumped libffi version to 3.4.2 #688 (misl6)
- Fixes libffi 3.4.2 not linking correctly (missing symbols) on iOS Simulator #695 (misl6)
- Update dpi for latest iPhone models #707 (akshayaurora)
- Our self-hosted Apple Silicon runner now has been migrated to actions/runner v2.292.0 which now supports arm64 natively #710 (misl6)
- Update xcassets to include mandatory icons in latest xcode #717 (tito)
- Bump cookiecutter from 1.7.2 to 2.1.1 #714 (dependabot)
- Use shutil.which instead of sh.which #735 (misl6)
[1.3.0] - 2022-03-13¶
Added¶
- Add native support for Apple Silicon #660 (misl6)
- Makes _bz2 module available #658 (misl6)
- enable sdl2 image provider to allow image to be saved from widget #656 (brentpicasso)
- Add MANIFEST.in #651 (misl6)
- Add official OSI name in the license metadata #639 (ecederstrand)
- Enables buildozer to automatically install dependencies #638 (meow464)
- Add bitcode support #605 (kewlbear)
- Add the ability to clean a custom recipe, use the custom recipe instead of the bundled one, if available. #602 (misl6)
- Add an error to FAQ #586 (mvasilkov)
Removed¶
- Remove NO_CONFIG and NO_FILE_LOG #670 (akshayaurora)
- Remove i386 and armv7 references from docs and code. Both are unsupported since a while. #610 (misl6)
Fixed¶
- Bump Kivy version to 2.1.0 #679 (misl6)
- CI: Use a Kivy-ios installation for tests, instead of the legacy toolchain.py #653 (misl6)
- Fix outdated freetype url #632 (misl6)
- Bump sdl2_ttf to 2.0.15 to improve compatibility with kivymd #624 (brentpicasso)
- add leading whitespace for include file #623 (brentpicasso)
- fix icon generation #620 (akshayaurora)
- Switch to iOS 9.0 where applicable #611 (misl6)
- Pillow recipe rework + upgrade to version 8.2.0 #606 (misl6)
- Update numpy to version 1.20.2 #604 (misl6)
- Upgrade python version to 3.9.2 #601 (misl6)
[1.2.1] - 2021-01-26¶
Fixed¶
- Fixes hostpython3 recipe on MacOS 11.1 BigSur #581 (misl6)
- Fixes iOS Simulator on latest Xcode #571 (misl6)
- Fixes Pillow build on Xcode 12.2 #579 (misl6)
- Cookiecutter: Fixes header and library search paths on Release #582 (misl6)
[1.2.0] - 2020-12-26¶
Added¶
Advise on using a venv #495 (AndreMiras)
- Add custom recipes #417 (misl6)
- Add feature request template #557 (Zen-CODE)
- Add instruction for cleaning cache and the build directory #558 (Zen-CODE)
Removed¶
- Remove distribute recipe #507 (Zen-CODE)
- Remove PY2 legacy blocks from coockiecutter main.m #522 (Zen-CODE)
- Remove outdated pil and pkgresources recipes #524 (Zen-CODE)
- Removed broken packages listing for non-existing packages #525 (Zen-CODE)
Fixed¶
fixes flake8 errors post update #496 (AndreMiras)
- Updates netifaces recipe, leverages
python_depends#490 (AndreMiras) - Fix pillow recipe #498 (Zen-CODE)
- Fix/pillow #500 (Zen-CODE)
- Fix wekzeug recipe #501 (Zen-CODE)
- Fix markupsafe recipe #505 (Zen-CODE)
- Fix jinja2 recipe, remove from broken list #508 (Zen-CODE)
- Fix werkzeug recipe #509 (Zen-CODE)
- Pin flask version and dependencies #510 (Zen-CODE)
Updates libffi download link #516 (AndreMiras)
- Remove --ignore-installed #521 (misl6)
- Remove compile cruft after build #523 (Zen-CODE)
- Move Cython requirement into requirements.txt #531 (Zen-CODE)
- Fix/host setuptools3 #533 (Zen-CODE)
- Fixes CI #569 (misl6)
- Fix build on Xcode 12.2 #568 (misl6)
- Update openssl and ffmpeg version to fix build issues #562 (Zen-CODE)
- Update kivy to a post 2.0.0 version #575 (misl6)
[1.1.2] - 2020-05-11¶
Fixed¶
- Fixes (venv build) reference to SDL_main.h #493 (AndreMiras)
[1.1.1] - 2020-05-11¶
Added¶
Removed¶
- Removed Python 2 support #482 (AndreMiras)
Fixed¶
- Adds initial_working_directory #489 (misl6)
- Adds netifaces recipe, closes #239 #488 (AndreMiras)
- Uses contextlib.suppress to ignore exceptions #487 (AndreMiras)
- DRY via the find_xcodeproj() helper method #486 (AndreMiras)
- Uses cd context manager in Python3Recipe.reduce_python() #485 (AndreMiras)
- Uses Python 3 syntax #484 (AndreMiras)
- Also lints the tools/ folder #483 (AndreMiras)
- Migrates libffi build to Python 3 #481 (AndreMiras)
- Uses a couple of syntax shortcuts #479 (AndreMiras)
- Takes ToolchainCL definition outside the main #478 (AndreMiras)
[1.1.0] - 2020-05-05¶
Added¶
- Automatically publish to PyPI upon tagging #475 (AndreMiras)
- Dedicated setup.py test workflow #474 (AndreMiras)
Fixed¶
- Fixes a regression introduced during the linting #477 (AndreMiras)
- More fixes to Numpy so that the binary is accepted by the App Store #473 (lerela)
- Do not build known broken recipes #471 (AndreMiras)
- Fixes minor typos in the issue template #469 (AndreMiras)
- Activates venv before venv build #464 (AndreMiras)
- Fixes building in venv #462 (AndreMiras)
- Cleanup - Removes vendored deps #454 (misl6)
Changed¶
- Updates README.md with install/usage from PyPI #476 (AndreMiras)
- Moving to dedicated kivy_ios/ package directory #472 (AndreMiras)
- Bumps Cython version #470 (misl6)
- Uses new
cdcontext manager more #465 (AndreMiras)
[1.0.0] - 2020-05-02¶
- Initial release