Skip to content

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 ios has a "Team ID" check (#108). It reads the Team ID that --device and --release builds sign with, from KIVYFORGE_TEAM_ID or [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 Development certificate'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_profile is 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 Development certificate 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-info folder beside the app's code on macOS, Linux, Windows, Android and iOS. Inside the app, importlib.metadata now finds the app's name, its version (the same value as versionName and CFBundleShortVersionString), the entry points of [project.scripts], [project.gui-scripts] and [project.entry-points], and its modules through packages_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 an egg-info in app_dir, as old as that install. See Read your package metadata inside the app.
  • Changed: a development *.egg-info folder at the top of app_dir, and a .dist-info folder 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 build stages 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 the egg-info now reads the same names from the generated folder, current with the build; nothing to change. An app whose code looks for the egg-info folder itself should use importlib.metadata instead.
  • Changed: a setuptools project that declares entry points only in setup.cfg ([options.entry_points]) or setup.py (entry_points=), and none in [project], fails build, package and run with KF-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].name are checked as build backends check them, and each violation is a config error at its line in every verb: a console_scripts or gui_scripts group 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].dynamic lists entry-points, scripts or gui-scripts, kivyforge cannot know the entry points, so it generates nothing, leaves app_dir as it is, and warns with KF-METADATA-NOT-GENERATED. Its context names the reason and the development egg-info that 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 xcodebuild command kivyforge ran) succeeded but left the .app with an invalid signature: codesign --verify --deep --strict reported "a sealed resource is missing or invalid" and named every Frameworks/<module>.framework binary. 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/ and Frameworks/) 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 next kivyforge build, which regenerates the script phase.

iOS: the default deployment target is 16.0

  • Fixed: with Xcode 27, kivyforge build -p ios failed 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 init writes 16.0, and the hello-world example, 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, whatever deployment_target said. 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 ios has a "Deployment target" check. It reads the range the selected Xcode accepts from the iOS SDK and fails when deployment_target is 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 to pyproject.toml makes build stop with KF-LOCK-DRIFT (exit 4), so run kivyforge lock -p ios after the edit.
  • Not set: you need no edit, but your app's minimum iOS rises from 13.0 to 16.0 (MinimumOSVersion and IPHONEOS_DEPLOYMENT_TARGET), so devices below iOS 16 cannot install new builds. If your dependencies run on iOS 15 (Kivy's do not), set deployment_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, so build, package and lock --check accept a lock made before this change. A re-lock does ask pip for ios_16_0_* tags instead of ios_13_0_*. Wheels built for an older iOS still match, so the kivy and pyobjus wheels (tagged ios_13_0) resolve to the same files. A package that publishes wheels for several minimum iOS versions may resolve to a different file, which makes lock --check report the lock as stale; run kivyforge 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 --check does that resolve even when the file hash still matches, so a range fails the check when a newer release appears. build and package keep 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 android shows 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 android without --debug now ends with a line saying Gradle did not run and naming the commands that produce an APK (#106). Before, it ended on Generated <app>-android with 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 for package. If you run ./gradlew assembleDebug yourself on that project, the debug APK you get contains the release payload. Use build --debug instead. --json output 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, package and run ask pip to prepare the project's metadata, in an isolated environment, and print the version they got. This works whether kivyforge was installed with uv tool, pipx or as a project dependency. lock, doctor, status and clean never 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 VERSION on build, package and run, and the KIVYFORGE_APP_VERSION environment variable (the flag wins), to set the version without running the build backend. --no-build-isolation resolves a dynamic version with a backend installed alongside kivyforge, offline.
  • Added: --json envelopes from build and package carry data.version: {"value", "source"}, where source is override, static or backend. It is recorded before the build starts, so failed builds keep it.
  • Added: doctor reports 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, or build -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 as 1.0rc1 or 1.2.3.4. App Store Connect refuses such a version at upload (ITMS-90060). Other iOS exports and macOS package warn instead, and debug builds are unaffected. Migration: a static pre-release version used to fail at upload; it now fails at package. Build from a release version, pass --app-version, or export with --export-method ad-hoc or development.
  • Changed: [project].version must follow PEP 440, and kivyforge writes its normalized form. Migration: v1.2 becomes 1.2 in versionName, the plists and file names; 1.0-beta becomes 1.0b0; a local segment is lowercased with - turned into . (1.0+Build-5 becomes 1.0+build.5). A version PEP 440 can't parse, such as 1.0.0-SNAPSHOT, now fails to load; pip already refused to build such a project.
  • Changed: a + in the version becomes _ in Linux .AppImage and Windows dist folder names, since + turns into a space in URL query strings. Migration: 1.0+local now packages as ...-1.0_local-....
  • Changed: listing name, dependencies, authors, description or requires-python in [project].dynamic is now an error. kivyforge reads those fields itself, and a dynamic one used to load as absent: a dynamic = ["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 invoke0 method is unchanged. kivyforge no longer caps the pyjnius version. Instead of guessing from the version number, build now checks the installed pyjnius wheel for the invoke0 signature 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 android adds pyjnius>=1.7.0 instead of pyjnius>=1.7.0,<1.9 when 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, and doctor only 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's llvm-strip mishandles. 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-MISALIGNED if 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; set strip_native_libs = true to strip debug builds as before.
  • Changed: a wheel whose libraries directory holds a .so not named lib*.so (for example libgfortran-040039e1.so.5) now fails the build with a message naming the file. Android only packages lib*.so files, 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 with KF-ANDROID-WHEEL-LAYOUT, naming the wheels. Their libraries live under opt/lib, which kivyforge cannot load from. Before, the build failed with a misleading "ABI content skew" on their .a files, 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_v8a wheel) now fails the build with KF-ANDROID-WHEEL-ARCH, instead of failing to load on the device.
  • Fixed: run -p android --release without 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.so or blosc2/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 with KF-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-urllib User-Agent. Every request kivyforge makes now identifies itself as kivyforge/<version>.
  • Changed: when an index publishes no SHA-256 for a wheel, lock now says so and gives the two fixes: use an index that publishes hashes, or put the wheels in a find_links directory, 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 (and run'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, after clean --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, which lock resolves through, is left alone (pip cache purge clears it). After clean --cache, lock usually stays fast and shows no downloads.

Android: pyjnius is included automatically

  • Fixed: an Android app whose dependencies didn't list pyjnius built without complaint and then crashed at launch with No 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. Now kivyforge lock -p android adds 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 pyjnius requirement whose marker leaves Android out (for example pyjnius; sys_platform == 'win32') counted as declared, so nothing was added, pip skipped it for Android, and build stopped with KF-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. Run kivyforge lock -p android --update. Projects that already declare pyjnius need no change.

Android: lock and build show their downloads

  • Fixed: kivyforge lock -p android printed 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. Without sdkmanager, or with its license not accepted, build says 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 --json output 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 res folder with dest = "app/src/main/res", keeping subfolders and qualifiers. (#71)
  • Added: a file that replaces one kivyforge generates, such as res/values/styles.xml or the launcher icon, is now reported ([include] ... replaces the file kivyforge generated, and KF-ANDROID-INCLUDE-OVERRIDE under --json). It used to be silent.
  • Added: the build stops, saying what to fix, when an included strings.xml or styles.xml leaves res/values/ without app_name or Theme.Kivyforge, which the generated manifest uses. That used to fail later in Gradle with an AAPT error that did not mention include_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, until kivyforge clean.

Android: run follows the app's log

  • Changed: kivyforge run -p android now 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 mentioning SDL or kivyforge) and dropped the app's own Android crash report.
  • Added: if the app never starts, run prints the device log since launch and fails, instead of showing an empty capture.
  • Migration: a script that runs kivyforge run -p android and expects it to return must add --no-follow, which keeps the one-snapshot behaviour (now filtered to the app's process). run --smoke is unchanged.

Android: switching kivy_generation no longer needs kivyforge clean

  • Fixed: rebuilding after changing [tool.kivy.android].kivy_generation failed to compile until kivyforge clean was run. Going from 3 to 2 failed with duplicate class: org.libsdl.app.SDLInputConnection, because the SDL3 Java files were left in the generated project. Going from 2 to 3 failed with cannot 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 discards app/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 used java.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 jnius raised RuntimeError: 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 android could crash with AttributeError: 'NoneType' object has no attribute 'splitlines' after launching the app. adb output is now read as UTF-8.
  • Added: kivyforge run --smoke also 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 android package, 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 own mActivity, api_version, open_url and 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 needs pyjnius. See Use the android package.
  • Fixed: on Kivy 2.3.1, tapping a TextInput quit the app, and so did the back key, App.stop() and App.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 with KIVY_AUDIO. A TextInput with input_type = "number" (or "mail", "tel", ...) gets the matching keyboard.
  • Changed: kivyforge lock fails with KF-ANDROID-DEPENDENCY-CONFLICT when a dependency is named android, and kivyforge build fails with KF-ANDROID-APP-CONFLICT when app_dir has an android.py or an android/ package (or with KF-ANDROID-DEPENDENCY-CONFLICT when 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 android from your dependencies, for example a package index's android wheel or an android carried over from a buildozer requirements line. Rename an android.py or android/ of your own.

Android: services have start, stop and mService

  • Added: each generated service class has static start(context, argument) and stop(context) methods. A foreground service also gets the five-argument start(context, icon, title, text, argument), which replaces the configured notification text for that run, and is started with startForegroundService. Inside a service, autoclass("org.kivy.android.PythonService").mService is the running service. These match python-for-android's services, so ported code only changes the class name to org.kivy.android.Service<name>. See Add a background service and the new examples/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:configChanges list as python-for-android, including uiMode, 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:configChanges through [tool.kivy.android.manifest].activity keeps 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 = 2 app'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 with fullscreen = true only the notch is padded. Kivy 3.0 apps are unchanged: they stay edge to edge and pad themselves with kivy.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].orientation said. 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].orientation to Kivy as KIVY_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 portrait but relied on rotating anyway must now list the orientations it supports.

Android: fullscreen hides the system bars

  • Added: [tool.kivy.android].fullscreen = true runs 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's fullscreen = 1, and kivyforge init now names it when it finds a buildozer.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 missing fullscreen as 1, so an app migrated from buildozer that never set the key needs fullscreen = true to 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-ERROR and exit 1, with a --json envelope.
  • 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 (or package, 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's openjdk-21-jdk-headless does), the next build still failed with "does not provide the required capabilities: [JAVA_COMPILER]" until kivyforge clean stopped the daemon. Every Android verb that runs Gradle now checks first, the same way doctor does, and fails before Gradle starts when there is no java or the java Gradle would run has no javac beside it. (Issue #61.)
  • Changed: those two cases now fail with KF-TOOLCHAIN-MISSING and exit 3 ("fix the machine"), with context.tool set to java or javac. Before, Gradle ran and failed with KF-BUILD-TOOL-FAILED and exit 5. A JDK of an unsupported version is still reported by Gradle, as before.
  • Migration: a script that treated exit 5 as "no JDK" should check for exit 3 and KF-TOOLCHAIN-MISSING. If a daemon from an earlier kivyforge release is already stuck, run ./gradlew --stop in <app>-android/ once (or kivyforge 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 android reported the JDK check as passed, because the version is in range. The build then failed at compileDebugJavaWithJavac with "does not provide the required capabilities: [JAVA_COMPILER]". The JDK check now also requires a javac beside the java Gradle will run (following symlinks, as Debian's /usr/bin/java needs), and fails with a hint to install a full JDK. (Issue #47.)
  • Migration: none needed. If doctor now fails the JDK check, install a JDK (sudo apt install openjdk-21-jdk-headless on Debian or Ubuntu) and point JAVA_HOME at it.

Android: BOMs and Gradle plugins (enables Firebase)

  • Added: [tool.kivy.android.gradle].platforms lists BOM coordinates, such as com.google.firebase:firebase-bom:34.15.0. Each is imported with Gradle's platform(), and a dependencies entry may then omit its version (com.google.firebase:firebase-analytics). kivyforge lock records 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. With include_files staging google-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 doctor with no -p on a project whose only overlay is [tool.kivy.android] (or any single non-host platform) fell back to iOS and reported missing [tool.kivy.ios] table, Xcode not found and xcode-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 -p or set KIVYFORGE_PLATFORM to 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 run sdkmanager --licenses. In command-line tools 23 and later sdkmanager is a wrapper around Google's Android CLI, and --licenses does nothing (exit 0, no license recorded), so the warning never cleared. The hint now says to install any package (such as sdkmanager platform-tools), which records the license; --licenses is still named for older tools.
  • Docs: the quickstart has a new "SDK licenses and sdkmanager" section. It also notes that the new sdkmanager exits 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 doctor now 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 exact sdkmanager 'cmake;3.22.1' command.
  • Changed: the generated app/build.gradle pins externalNativeBuild.cmake.version to 3.22.1 (AGP's own default), so the build and doctor ask 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.gradle now sets sourceCompatibility and targetCompatibility to VERSION_17 (was VERSION_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 invoke0 range 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 bare pyjnius dependency re-locks to it, and build and doctor used to reject that lock.
  • Fixed: the build/doctor message 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 android now warns (and records a KF-LOCK-WARNING diagnostic under --json) when [tool.kivy.android].kivy_generation does not match the Kivy it resolved, for example kivy_generation = 3 locking Kivy 2.3.1. Previously only doctor noticed, after the lock was written. The lock is still written.
  • Docs: kivy_generation selects 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. orientation is 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 keeps orientation.
  • Changed: the exclude list in the desktop (macOS, Linux, Windows) templates is now an optional, commented-out size trim. It used to be active and dropped requests and its dependencies, which breaks UrlRequest. iOS and Android keep the active list.
  • Changed: the iOS and Android templates write extra_index_urls for the kivy-mobile-wheels index uncommented. Without it kivyforge lock cannot find a Kivy wheel. init now assumes Kivy is a dependency: the exclude list and this line are always written, instead of only when [project].dependencies names kivy. The Linux template notes that archs = ["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 suggest kivy>=2.3.1 and say an iOS app needs Kivy 3.0.
  • Migration: existing projects are untouched. A project that relied on the desktop exclude default 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 lock now names the package in its own headline (for example no 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 otherwise find_links or 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_urls line, and says lock uses kivyforge's own pip, not the app environment's. The init template comment for that line no longer says "until they are on PyPI".

run: progress goes to stderr

  • Changed: kivyforge run printed 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 for build and package. 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, and devicectl adds 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 new android_bundle job now runs kivyforge package -p android -f aab with 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 android passed any java that existed. On a Mac with no JDK, Apple's /usr/bin/java stub 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". doctor now runs java -version and requires a JDK from 17 to 23.
  • Fixed: with ANDROID_HOME unset, kivyforge did not look in ~/Library/Android/sdk, where Android Studio installs the SDK on macOS. doctor, the build's sdk.dir and run's adb lookup 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 doctor checks.
  • Fixed: run -p android --smoke without --release staged the release payload, so with the default byte_compile = "release" it tested byte-compiled, stripped Python inside a debug APK. It now stages the debug payload, the same Python run installs. --smoke --release is unchanged.

The missing-pyproject.toml hint no longer points at init to create one

  • Fixed: a verb run without a pyproject.toml suggested running kivyforge init to create one, but init does not create one: it stops and asks for a [project] table first. The hint now says to write the [project] table, then run kivyforge init -p <platform>.

The dice-roller example no longer names the author's Developer ID

  • Changed: examples/desktop/dice-roller no longer has a [tool.kivy.macos.signing] table, so package -p macos ad-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.toml that 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, windows and android extras. Their only content was Pillow.
  • Migration: install plain kivyforge. An old command such as pip 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 init finds a buildozer.spec and no pyproject.toml, it prints how each buildozer setting maps onto kivyforge. Several entries pointed at keys kivyforge does not read:
  • android.gradle_dependencies now maps to [tool.kivy.android.gradle].dependencies.
  • android.add_jars now maps to [tool.kivy.android.native.jars].
  • android.add_activities now maps to [[tool.kivy.android.activities]].
  • android.add_src is 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_color and 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 --device could 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 --device now chooses the phone before building and passes it to xcodebuild as -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 --device has 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 clearing team_id), the next build still carried the old value in the generated .xcodeproj, which kivyforge updates in place rather than recreating. With auto_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 --device refused a single connected phone with "multiple paired iOS devices found", because Xcode 27's devicectl also lists simulators as paired iOS devices. --device now ignores simulators, so one connected phone is picked without --destination.
  • Fixed: --destination rejected the UDID that run --list-devices prints. It now accepts the UDID, as well as the name and devicectl's own identifier.
  • --device --destination matches 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 --simulator instead of "no device matches".

iOS: a pinned profile Xcode will refuse stops the build up front

  • Changed: a device or release build, run --device, and package now stop before xcodebuild in two cases where Xcode refuses the pinned profile. Before, both built the whole app and then failed (exit 5) with Xcode's error, while doctor reported PASS:
  • provisioning_profile set with auto_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 under auto_signing = false. Manual signing requires a profile created at developer.apple.com.
  • doctor -p ios now FAILs the Provisioning profile check in both cases.
  • package now runs the entitlements check that build already ran, so an entitlement the pinned profile does not grant fails before the archive instead of at export.
  • KF-ENTITLEMENTS-UNGRANTED is 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, set auto_signing = false and pin a manually created profile; or remove provisioning_profile and let Xcode manage signing. Re-lock afterwards. Simulator builds are unaffected.

iOS: run streams the app's console

  • Fixed: run -p ios launched 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 macOS run does, until the app exits or you press Ctrl+C. On a device the launch passes devicectl --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 aarch64 suggested x86_64, which then crashed with a traceback (no --json envelope) because no lock pins an x86_64 simulator slice. The error now suggests arm64 only.
  • 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 --json envelope, and says to re-lock.

macOS and Linux: the built bundle is readable by other users

  • Fixed: the macOS .app and the Linux AppDir were created mode 0700, so only the user who built them could open them, and cp -R carried that into a DMG or /Applications. They now get the mode a plain mkdir would (0755 under 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.
  • open help, 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-method now say which platforms read them. package --signing-identity applies to iOS and macOS only; Windows signs with [tool.kivy.windows.signing].thumbprint, and Android with --keystore.
  • Changed: build and package now refuse those options, and package'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 X succeeded 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 as deployment_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 Android manifest attribute 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 windows now 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 reports KF-PATH-DEPTH in --json output. 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 longPathAware would not help: Windows refuses to start an executable from a folder too long for its own path even with long paths enabled, and the bundled python.exe, which does the file access, already opts in.

iOS: provisioning_profile works as documented

  • Fixed: [tool.kivy.ios.signing].provisioning_profile was read two incompatible ways. Xcode received it as a profile name or UUID (the documented form), but doctor and the entitlements pre-flight treated it as a file path. A name or UUID made doctor FAIL ("not found") and silently skipped the entitlements check; a path passed doctor and then failed the Xcode build.
  • A name or UUID is now looked up among the profiles installed on the Mac, so doctor and the entitlements check use the same profile Xcode signs with.
  • A path to a .mobileprovision is also accepted: kivyforge reads the file and passes its UUID to Xcode. doctor warns 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: build and package rejected --arch aarch64 as a usage error, although the Linux spec documents package -p linux --arch aarch64. With archs = ["x86_64", "aarch64"] the Raspberry Pi AppImage could only be produced by reordering the list. The shared --arch choices now include aarch64; with no --arch, the first locked arch is still the default.
  • run still does not accept aarch64: that arch is cross-built only, and run launches the result on the build host.
  • build -p ios --arch aarch64 now fails with a clear message instead of deriving a nonexistent iOS platform tag.

Install: pip is now a dependency

  • Fixed: uv tool install kivyforge produced an install where every kivyforge lock failed. kivyforge runs python -m pip to 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 under docs/design/ stay unpublished.
  • The host matrix, exit codes, and KF-* diagnostic vocabulary are generated at build time from kivyforge's own capabilities/report modules, and the CLI reference from the click command tree, so the site cannot drift from the tool. mkdocs build --strict runs in CI and fails on a broken internal link, a missing nav page, or a KF-* 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/hp70092A beside h/hp70092a). Building a Linux app onto a case-insensitive filesystem, such as /mnt/c from WSL2, used to die partway through staging with a bare shutil.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 --release now works on the desktop backends, as it has on Android since the entry below. Previously run on Linux, macOS and Windows always built the dev flavor, so byte_compile/strip_source (both release-only by default) could only be reached through kivyforge package — never through the command used for day-to-day iteration. That gap is where the Linux AppRun defect 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>/ tree run always launches, so a later plain run rebuilds it with readable source.
  • --release --no-build is refused: --no-build launches whatever the last build left, and nothing records whether that was a release build.
  • run -p ios --release is refused rather than ignored — an iOS release build is an archive and a signed .ipa, not a simulator install. Use kivyforge build --release or kivyforge 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 (and lock --check, which re-resolves) now sends GH_TOKEN or GITHUB_TOKEN as a bearer token when either is set, raising the ceiling to 5000/hour. GH_TOKEN takes precedence, matching the gh CLI. 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 text Rock &amp; 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_gradle job now asserts that AGP's merged release manifest still contains what pyproject.toml declared — 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 verify on the release APK at the project's own min_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 --release now builds and launches the release variant, not just --smoke --release. Previously run always built debug, so byte_compile/ strip_source (both release-only by default) could only ever be exercised through kivyforge 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, so run --release never 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 with Signing 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 .pbxproj build settings. DEVELOPMENT_TEAM now also goes out as an xcodebuild command-line override (as CODE_SIGN_IDENTITY already did) for --device builds and --release archives, which is what the package's own targets actually read.
  • An earlier attempt at this fix (also writing DEVELOPMENT_TEAM/ CODE_SIGN_STYLE to 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 real firebase-ios-sdk package, 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 (FirebaseCore product, firebase-ios-sdk 11.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]'s embed field now defaults to false for a url (remote) package; a path (local) package is unchanged and still defaults to true. A remote package's library product is almost always automatic, which Xcode resolves to static for a standalone consuming target — there's no .framework for Xcode to copy, so the old embed = true default failed at xcodebuild build with The file "<Product>" couldn't be opened because there is no such file for most third-party packages, Firebase's included. A local path shim is author-controlled (its own Package.swift can deliberately declare type: .dynamic) and keeps the old default.
  • Migration: if a remote package's product really is a dynamic framework (check its own docs — Sentry's Sentry product is one example), set embed = true explicitly; it no longer comes for free.
  • Reported by an early user after adding FirebaseAuth; reproduced against the real package and confirmed embed = false fixes it. See docs/design/dev/ios-firebase-spm-signing-findings.md for the full investigation, including why a reactive fix (matching xcodebuild'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.xcframework now fails to compile if its SDL3 headers are somehow missing (a broken vendoring step, a manually edited HEADER_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 android writes both overlays into pyproject.toml in 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 one init invocation per platform.
  • --json's shape is unchanged for the common, single--p case (platform + data.action/data.tables). With more than one -p, the envelope's platform is null and results are listed under data.results as {"platform", "action", "tables"} per platform, in the order given.
  • Every other verb (build, run, package, doctor, status, ...) is unaffected — their -p still 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 calls App().run() unconditionally at module top level (the style every in-repo example uses), nothing changes for you.
  • Breaking for one specific pattern: an entry_point pointing at a package (not a module) now requires a __main__.py on every platform, same as python -m mypackage. Before this change, Android and iOS ran the package's __init__.py instead. Add a __main__.py, or point entry_point at an explicitly-named module instead (e.g. entry_point = "myapp.start").
  • Also changes: the entry module is no longer left in sys.modules after startup (mobile only — desktop already worked this way). Code that expects to import its own entry module again by name after startup will now re-execute it instead of getting the cached one.
  • Implementation: Android's main.c and iOS's kivyforge_bootstrap.m both now run the entry module via runpy.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 kivy costs 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, inspect and linecache read. strip_source still applies only to your app and its site-packages.
  • Follows the existing [tool.kivy.<platform>.build_settings].byte_compile setting, so it applies to package by default and to every build when that is true. 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-input at the lock resolvers and the wheel staging installs: an index that wants credentials is the one case here that genuinely prompts.
  • kivyforge run is 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].archs accepts "aarch64" (opt-in; the default stays x86_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.
  • appimagetool is 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. --json publishes:
  • platforms[]: each target's archs, package_formats, default_package_format, aliases, the host it is the default target for, and hosts: 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 --help states it.
  • verbs[]: every registered verb and whether it accepts --json.
  • exit_codes and diagnostic_codes: the reserved exit taxonomy and the published KF-* 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 run now takes --json and --no-color. Each reports what it did rather than that it did something:
  • clean: data.removed lists the generated trees actually deleted (relative, posix), plus cache_flushed and gradle_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 — and data.pyproject.
  • upgrade: data.refreshed names each re-fetched artifact and data.skipped counts vendored (path-pinned) entries. A hash mismatch half way through still reports the artifacts already refreshed.
  • open: data.project is the project that was opened.
  • Two new diagnostic codes, both warnings on an ok: true run: KF-IDE-NOT-FOUND when open finds no Android Studio launcher on PATH (the project is still generated, just not launched), and KF-DEPENDENCY-DRIFT when init sees an installed dependency outside its declared specifier.
  • open -p ios now 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)." and upgrade's per-artifact "Refreshing …" lines. What each verb produced stays on stdout.

Breaking: build and package progress moves to stderr

  • For build and package, 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 Android run and --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 xcodebuild or appimagetool fails, 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. appimagetool failures 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 --json and --no-color, like the other verbs. The envelope's data.artifacts lists what this run produced, as {"path", "kind"} with the path relative to the project root and kind one of appimage, apk, aab, app, ipa, folder, project.
  • Only products this invocation finalised are listed, including on failure: a build that fails after generating the project still names the project; a package that fails names nothing, never a previous run's artifact.
  • Success-path warnings arrive as diagnostics on an ok: true envelope: KF-SIGNING-UNCONFIGURED, KF-BYTECOMPILE-NO-INTERP, KF-MANIFEST-POLICY and KF-ENTITLEMENTS-UNGRANTED.
  • Failures now say what kind of failure they are. build and package used to exit 1 for 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_64 removed from macOS and the iOS Simulator, as a host and as a target. macOS 27 stops installing on Intel hardware, GitHub's macos-*-intel runners 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].archs accepts only ["arm64"] (the new default), and [tool.kivy.ios].simulator_archs only ["arm64"]. Both reject x86_64 with a migration message naming the fix.
  • universal2 builds are gone along with the lipo merge machinery, and --arch universal2 is 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.
  • universal2 wheels 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 own build_settings already does: byte_compile and strip_source (each "release" | true | false, default "release"). "release" applies only to kivyforge package — build/run always keep readable .py for fast iteration.
  • Only the app sources and installed site-packages are compiled/stripped; the staged Python stdlib is never touched (readable tracebacks, inspect/linecache stay 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 kivyforge itself on a matching CPython minor, else the default degrades to shipping source with a note (byte_compile = true fails 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 .py unconditionally until now.

iOS source stripping

  • New [tool.kivy.ios.python.build_settings] table (nested under python, since [tool.kivy.ios].build_settings already names the Xcode passthrough table): the same byte_compile/strip_source release tri-state as Android and the desktop backends, covering both pip-deps (third-party wheels) and the app's own source.
  • build/run are unaffected — app/ stays a symlink to app_dir for fast iteration, exactly as before.
  • kivyforge package now stages app/ as a real, disposable copy instead of a symlink and compiles/strips that — the user's actual source tree is never touched. Switching back to build/run afterward reclaims the copy for the symlink automatically.
  • The staged Python.xcframework stdlib is never touched (it's a prebuilt, opaque library either way).
  • Python.xcframework ships no standalone interpreter to shell out to, so compilation always uses the kivyforge-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 android target. kivyforge lock/build/run/package -p android resolves into pylock.android.toml, assembles the official python.org Android runtime (per ABI) plus per-ABI android_* 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, adaptive icons/splash (AndroidX SplashScreen), permissions (+ implied <uses-feature> synthesis), services/activities/intent_filters, the manifest passthrough (+ raw-XML), native.aars/.jars and Maven gradle dependencies, include_files, src (Java/Kotlin), signing (v1–v4 schemes), and build_settings (byte-compile/strip/R8/debug-symbols). Loader-validated (rules 1–21).
  • kivyforge-owned bootstrap — a generated org.kivy.android.PythonActivity (extends stock SDL2 SDLActivity), the pyjnius NativeInvocationHandler matched pair (with a hard invoke0 contract gate), the Kivy-compat shims (org.renpy.android.Hardware + mActivity), an SDL_main native launcher (NDK-compiled per ABI), and a sys.meta_path finder over flattened extension .sos in jniLibs/. pyjnius ships as a prebuilt Java-free wheel (SDL- agnostic, JNIEnv resolved at runtime).
  • Four artifact channels — android_* wheels (PyPI / supplemental indexes / vendored find_links), the python.org runtime, .aar/.jar archives, and Maven coordinates (Gradle resolves; the graph is SHA-256-recorded in the lock).
  • Mandatory signing — debug builds use the auto-managed debug keystore; package requires a release keystore (env-var passwords, never in pyproject.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 pyjnius invoke0 glue 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), and qr-maven (Google ZXing via the Maven channel + a Python-implements-Java invoke0 round-trip), plus examples/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-android and hello-sdl3 carry their locks committed as validation evidence; the rest resolve like any other dependency.
  • SDL3 / Kivy 3.0 support. kivy_generation = 3 renders 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/.so version mismatch rather than the silent black-screen failure that mismatch would otherwise produce.
  • Validated on real hardware. kivyforge run --smoke passes 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 windows target. kivyforge lock/build/run/package -p windows resolves into pylock.windows.toml, bundles a relocatable python-build-standalone MSVC CPython plus win_amd64 wheels, and produces a run-from-folder onedir: a windowed-subsystem launcher .exe beside 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 additive win-arm64 later. 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 shared extra_index_urls/find_links/exclude.
  • Prebuilt, reproducible launcher — a single vendored windowed launcher .exe (built once from launcher.c with MSVC /Brepro + /RELEASE + /EMITTOOLVERSIONINFO:NO, SHA-256 pinned, byte-reproducibility gated in CI — the last flag drops the PE "rich header", whose serviced cl/link build stamp otherwise differs between runner images sharing one VCTOOLSVERSION) is parameterized per app only by its resources (icon + version) via the vendored rcedit — no per-build compiler. The launcher self-locates, sets the AppUserModelID, hands the console off on run while suppressing the flash on double-click, and spawns the bundled python.exe under 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), and kivy_deps.* DLLs land where the loader finds them. The vcruntime140*.dll are staged from the PBS prefix (empirically msvcp140 is 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\ with os.add_dll_directory and prepends it to PATH (helpers run by name, DLLs load by name).
  • Unsigned folder packaging — package -f folder atomically copies the onedir into dist\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 Signer protocol with SigntoolSigner (signtool sign /sha1 <thumbprint> /fd SHA256 /tr <url> /td SHA256, /sm for store_scope=machine, always timestamped) and NullSigner; identity is always a cert-store thumbprint, never a .pfx. A deterministic PEP 440 → four-part numeric FILEVERSION mapping feeds the version resource. Signing targets the dist copy only, after the resource patch.
  • kivyforge doctor -p windows — Windows host, long-path support, app_id style, arch coverage, app icon, native-binary sources/collisions/arch, resource assets, reachable hosts (incl. the signing timestamp server), signtool availability, and the signing certificate (in the configured store_scope store).
  • Examples — desktop-viewer, dice-roller, notes, and hello-native gained [tool.kivy.windows] overlays (hello-native adds a greet.dll + roll.exe native channel); examples/verify-windows-examples.ps1 mirrors the desktop verifier's clean→lock→doctor→build→run→package→launch loop.
  • CI — windows-latest unit tests, a launcher reproducibility + vendored-asset gate, and a self-signed signtool sign→verify loop.

Linux backend (AppImage / AppDir)

  • New linux target. kivyforge lock/build/run/package -p linux resolves into pylock.linux.toml, bundles a relocatable python-build-standalone gnu/glibc CPython plus manylinux wheels, and produces an AppImage (the package default) with a run-from-folder AppDir as the substrate/fallback (package -f folder). x86_64 only this phase; the config stays list-shaped for additive aarch64 later.
  • [tool.kivy.linux] overlay — app_id (reverse-DNS; names the generated .desktop, Icon=, and StartupWMClass), archs, optional glibc_floor, python.version, icons.source, desktop.categories, and the shared extra_index_urls/find_links/exclude. Invalid app_id characters 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 host libfuse2, and APPIMAGE_EXTRACT_AND_RUN=1 means the build host needs no FUSE either. A generated AppRun (POSIX shell) sets PYTHONHOME/PYTHONPATH/SDL_VIDEO_*_WMCLASS — no compiled launcher, no native build at package time.
  • Generated desktop integration — .desktop entry + hicolor icon set from pyproject.toml (Pillow for resize via the optional kivyforge[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/libEGL via ldconfig, display session, glibc floor vs. runtime, manylinux arch coverage, app icon, generated .desktop validity (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, and notes gained [tool.kivy.linux] overlays + committed pylock.linux.toml; examples/run-examples.sh -p linux builds and runs them.
  • CI — added an ubuntu-latest headless job (the cross-platform core now runs on both macOS and Linux hosts). Artifact cache respects $XDG_CACHE_HOME on 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's pyproject.toml; no more toolchain.spec or requirements.txt. Config validation is strict and fail-fast: [tool.kivy.ios.python].version is required (no hidden default at lock time), native xcframework source paths may not escape the project, signing flags must be real booleans (a quoted "false" is rejected, not coerced to true), and [tool.kivy.ios.xcode.build_settings] values must be strings.
  • Dependency locking — kivyforge lock resolves pure-Python and iOS wheel dependencies and writes pylock.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.whl wheels; kivyforge build downloads and integrates them.
  • Xcode project generation — kivyforge init generates a complete .xcodeproj via pbxproj; kivyforge open launches 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's kivy.mobile module (kivy/kivy#9331), shipped in the Kivy iOS wheel.
  • Native iOS dependencies — declare Swift Package Manager packages and native .xcframework archives under [tool.kivy.ios.native]. kivyforge lock pins SPM packages to a concrete revision and resolves each xcframework archive (by URL or repo-relative path) into a SHA-256 + enumerated slice list in pylock.ios.toml; kivyforge build fetches, verifies, and wires them into the Xcode project's Link / Embed phases per each entry's link/embed intent, 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 — toolchain now dispatches to init, lock, build, run, open, upgrade, clean, status, and doctor; 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 lock relies on to resolve iOS wheels; kivyforge doctor flags older pip and kivyforge lock fails 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

Full Changelog

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_version flag during linking instead of the deprecated and now removed) -ios_simulator_version_min #970 (misl6)
  • Bump libpng version to 1.6.48 #967 (misl6)
  • Disable iOS armv7 build for libffi #966 (misl6)
  • Add SDL3, SDL3_ttf, SDL3_mixer, SDL3_image recipes for Kivy 3.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-m1 self-hosted runner, as now is supported by Github Hosted runners via macos-latest tag. Use macos-13 for runs on Intel macs #909 (misl6)
  • (recipes/materialyoucolor): update to 2.0.9 #904 (T-Dynamos)

v2024.03.17

Full Changelog

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 materialyoucolor recipe #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: 0 for updated recipes checkout and use actions/checkout@v4 instead of actions/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 python3 recipe to 3.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 numpy version to 1.24.4 #856 (misl6)
  • Support ARM64 Simulator + Introduce build platform concept + Introduce xcframework #778 (misl6)

v2023.08.24

Full Changelog

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 Cython for now, as most of the recipes are incompatible with Cython==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 python3 via setup-python also for Apple Silicon macs #829 (misl6)
  • Bump Kivy version to 2.2.1 #828 (misl6)
  • Update libpng recipe and build SDL2_ttf vendored freetype with 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

Full Changelog

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-deps and --platform during 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 numpy version to 1.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

Full Changelog

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.executable instead of sh.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

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

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

[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

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

[1.1.2] - 2020-05-11

Fixed

[1.1.1] - 2020-05-11

Added

Removed

Fixed

[1.1.0] - 2020-05-05

Added

Fixed

Changed

[1.0.0] - 2020-05-02

  • Initial release