From abfa4b27502e7310167a46d68b18aeb7f4465bc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20P=C3=A9ron?= Date: Sun, 23 Aug 2026 19:26:31 +0000 Subject: [PATCH] gh-156109: Allow static, non-framework iOS builds A shared Python on iOS has to be packaged as a framework for App Store Connect to accept it. A static libpython is linked into the app binary and loads nothing at runtime, so the requirement does not apply to it, but configure refused that configuration outright. Refuse it only where it cannot work: a shared build with no framework. The LINKFORSHARED and MODULE_DEPS_SHARED framework references are gated on enable_framework, as the Darwin arm above already does, since a build without a framework has nothing to link against. Omitting the framework option behaves as --disable-framework, as it does on every other platform. A static build does come with restrictions - it cannot load extension modules at runtime, and so cannot use binary wheels - so require the user to opt into them explicitly. A non-framework iOS build now errors unless MODULE_BUILDTYPE=static is set (there is no framework for a shared extension module to link against) and --disable-test-modules is given (some test modules must be built as shared libraries; see Modules/Setup.stdlib.in). Those two options have defaults that point the other way, so they must be typed; the errors lead with --enable-framework, so that a forgotten framework option does not read as an invitation to build static. Document the build process and its limitations in a new section of Platforms/Apple/iOS/README.md, and note in Doc/using/ios.rst that the official iOS release artefact is a framework build. --- Doc/using/configure.rst | 10 +++ Doc/using/ios.rst | 7 ++ ...-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst | 5 ++ Platforms/Apple/iOS/README.md | 80 +++++++++++++++++-- configure | 30 +++++-- configure.ac | 25 ++++-- 6 files changed, 142 insertions(+), 15 deletions(-) create mode 100644 Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index 8b4940ceb9521a..88b5f35a796796 100644 --- a/Doc/using/configure.rst +++ b/Doc/using/configure.rst @@ -1342,6 +1342,16 @@ See :source:`Platforms/Apple/iOS/README.md`. Specify the name for the framework (default: ``Python``). +An iOS build configured without ``--enable-framework`` produces a static +``libpython``, for embedding directly in an app binary. Such a build cannot +load extension modules at runtime, and so does not support binary wheels; it +requires ``MODULE_BUILDTYPE=static`` and :option:`--disable-test-modules`, and +rejects :option:`--enable-shared`. See +:source:`Platforms/Apple/iOS/README.md` for the full list of limitations. + +.. versionadded:: 3.16 + iOS builds may be configured without a framework. + Cross Compiling Options ----------------------- diff --git a/Doc/using/ios.rst b/Doc/using/ios.rst index 31d9e2f2c816e7..80b9b3c3a07f06 100644 --- a/Doc/using/ios.rst +++ b/Doc/using/ios.rst @@ -142,6 +142,13 @@ should ensure these stub binaries are on your path. Installing Python on iOS ======================== +The official iOS release artefact is a framework build, distributed as an +``XCFramework``; this is the configuration described in the rest of this +document, and the only one that supports binary extension modules. Static +builds, where ``libpython`` and every extension module are linked directly into +the app binary, are also possible, with limitations; see +:source:`Platforms/Apple/iOS/README.md` for details. + Tools for building iOS apps --------------------------- diff --git a/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst b/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst new file mode 100644 index 00000000000000..4b8c1c197ddf5d --- /dev/null +++ b/Misc/NEWS.d/next/Build/2026-08-20-13-32-54.gh-issue-156109.ZTJHi9.rst @@ -0,0 +1,5 @@ +iOS builds may now be configured without a framework, producing a static +``libpython`` for embedding in an app binary. Such a build cannot load extension +modules at runtime, and requires ``MODULE_BUILDTYPE=static`` and +``--disable-test-modules`` to be requested explicitly. A shared iOS build must +still be a framework build. diff --git a/Platforms/Apple/iOS/README.md b/Platforms/Apple/iOS/README.md index faeeead1df03a2..ab0f53161d6c34 100644 --- a/Platforms/Apple/iOS/README.md +++ b/Platforms/Apple/iOS/README.md @@ -90,8 +90,13 @@ Python build for a single framework, the following options are available. installed. If `DIR` is not specified, the framework will be installed into a subdirectory of the `iOS/Frameworks` folder. - This argument *must* be provided when configuring iOS builds. iOS does not - support non-framework builds. + This argument is required for any iOS build that will be distributed, and + for any build that needs to load binary extension modules. + + Omitting it builds a static `libpython` for embedding directly in an app + binary, instead of a `Python.framework`. That configuration comes with + significant restrictions; see [Building a static + Python](#building-a-static-python) below. * `--with-framework-name=NAME` @@ -113,9 +118,11 @@ framework to contain non-library content, so the iOS build will produce a The `lib` folder will be needed at runtime to support the Python library. If you want to use Python in a real iOS project, you need to produce multiple -`Python.framework` builds, one for each ABI and architecture. iOS builds of -Python *must* be constructed as framework builds. To support this, you must -provide the `--enable-framework` flag when configuring the build. The build +`Python.framework` builds, one for each ABI and architecture. Unless you are +statically linking Python into your app (see [Building a static +Python](#building-a-static-python) below), iOS builds of Python *must* be +constructed as framework builds. To support this, you must provide the +`--enable-framework` flag when configuring the build. The build also requires the use of cross-compilation. The minimal commands for building Python for the ARM64 iOS simulator will look something like: ``` @@ -216,6 +223,69 @@ target, provide the version number as part of the `--host` argument - for example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64 simulator build with a deployment target of 15.4. +### Building a static Python + +The official iOS release artefact is a framework build. However, if you are +embedding Python in an app that links `libpython` at compile time, you can +instead build a static `libpython3.x.a`, and link that archive directly into +your app binary. + +The App Store requirement that binary modules be packaged as signed frameworks +does not apply to a static build, because a static build loads nothing at +runtime; but for the same reason, this configuration cannot use *any* binary +module that isn't compiled into the app binary. The restrictions that follow +from that must be opted into explicitly at configure time: + +* `MODULE_BUILDTYPE=static` is required. There is no framework for a shared + extension module to link against, so every extension module, including the + ones in the standard library, must be linked into `libpython`. + +* `--disable-test-modules` is required. Some test modules must be compiled as + shared libraries (see `Modules/Setup.stdlib.in`), so they cannot be built in + this configuration at all. + +A non-framework build is selected by omitting `--enable-framework`; +`--disable-framework` is accepted as an explicit spelling of the same thing. +Such a build is also static by default, as `--enable-shared` is off unless +requested; `--enable-shared` without a framework is rejected, since an iOS app +can only load a signed framework, never a bare dylib. + +The minimal commands for a static build targeting ARM64 iOS devices are then: +``` +export PATH="$(pwd)/Platforms/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin" +./configure \ + --disable-framework \ + --disable-test-modules \ + MODULE_BUILDTYPE=static \ + --host=arm64-apple-ios \ + --build=arm64-apple-darwin \ + --with-build-python=/path/to/python.exe +make +make install +``` +This produces a `libpython3.x.a` containing the interpreter and the standard +library's extension modules; `make install` installs that archive, along with +the standard library's Python source, into the location given by `--prefix`. + +#### Limitations of a static build + +* **Binary wheels cannot be used.** There is no `libpython` dylib for a + third-party extension module to link against, and a static Python has nothing + to `dlopen` in any case. Pure Python wheels work as normal; any package with a + C extension must be compiled into the app binary alongside `libpython`. + +* **The standard library's extension modules are not loadable modules.** They + live in the archive, not in `.framework` bundles in the app's `Frameworks` + folder, so the packaging described in + [Using Python on iOS](https://docs.python.org/3/using/ios.html) does not apply + to them. + +* **The test suite cannot be run as-is**, as the test modules are not built. + +* This configuration is not covered by the `Platforms/Apple` build script, nor + by CPython's CI. It is not the configuration used to produce official + releases. + ## Testing Python on iOS ### Testing a multi-architecture framework diff --git a/configure b/configure index 6b560fe6841b72..5c1224c307087d 100755 --- a/configure +++ b/configure @@ -4442,7 +4442,6 @@ then : case $enableval in no) case $ac_sys_system in - iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework @@ -4559,7 +4558,6 @@ then : else case e in #( e) case $ac_sys_system in - iOS) as_fn_error $? "iOS builds must use --enable-framework" "$LINENO" 5 ;; *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework @@ -8047,6 +8045,10 @@ fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $LDLIBRARY" >&5 printf "%s\n" "$LDLIBRARY" >&6; } +if test "$ac_sys_system" = "iOS" && test "$PY_ENABLE_SHARED" = 1 && test -z "$PYTHONFRAMEWORK"; then + as_fn_error $? "iOS builds must use --enable-framework; --enable-shared cannot be used without a framework, as an iOS app can only load a signed framework" "$LINENO" 5 +fi + # HOSTRUNNER - Program to run CPython for the host platform { printf "%s\n" "$as_me:${as_lineno-$LINENO}: checking HOSTRUNNER" >&5 printf %s "checking HOSTRUNNER... " >&6; } @@ -14540,7 +14542,11 @@ printf "%s\n" "#define THREAD_STACK_SIZE 0x$stack_size" >>confdefs.h fi LINKFORSHARED="$LINKFORSHARED" elif test $ac_sys_system = "iOS"; then - LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" + + if test "$enable_framework"; then + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi fi ;; OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; @@ -28220,8 +28226,8 @@ if test "$PY_ENABLE_SHARED" = "1" && ( test -n "$ANDROID_API_LEVEL" || test "$MA LIBPYTHON="-lpython${VERSION}${ABIFLAGS}" fi -# On iOS the shared libraries must be linked with the Python framework -if test "$ac_sys_system" = "iOS"; then +# On iOS the shared libraries must be linked with the framework, when built +if test "$ac_sys_system" = "iOS" && test "$enable_framework"; then MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" fi @@ -35333,6 +35339,20 @@ case $host_cpu in #( esac +if test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK" +then : + + if test "$MODULE_BUILDTYPE" != "static" +then : + as_fn_error $? "iOS builds must use --enable-framework; a non-framework build cannot build shared extension modules, and requires MODULE_BUILDTYPE=static" "$LINENO" 5 +fi + if test "$TEST_MODULES" != "no" +then : + as_fn_error $? "iOS builds must use --enable-framework; a non-framework build cannot build the shared test modules, and requires --disable-test-modules" "$LINENO" 5 +fi + +fi + MODULE_BLOCK= diff --git a/configure.ac b/configure.ac index 476f13c82bbb2b..dbefe8c36408cb 100644 --- a/configure.ac +++ b/configure.ac @@ -579,7 +579,6 @@ AC_ARG_ENABLE([framework], case $enableval in no) case $ac_sys_system in - iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework @@ -689,7 +688,6 @@ AC_ARG_ENABLE([framework], esac ],[ case $ac_sys_system in - iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; *) PYTHONFRAMEWORK= PYTHONFRAMEWORKDIR=no-framework @@ -1695,6 +1693,10 @@ else # shared is disabled fi AC_MSG_RESULT([$LDLIBRARY]) +if test "$ac_sys_system" = "iOS" && test "$PY_ENABLE_SHARED" = 1 && test -z "$PYTHONFRAMEWORK"; then + AC_MSG_ERROR([iOS builds must use --enable-framework; --enable-shared cannot be used without a framework, as an iOS app can only load a signed framework]) +fi + # HOSTRUNNER - Program to run CPython for the host platform AC_MSG_CHECKING([HOSTRUNNER]) if test -z "$HOSTRUNNER" @@ -3836,7 +3838,11 @@ then fi LINKFORSHARED="$LINKFORSHARED" elif test $ac_sys_system = "iOS"; then - LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED" + + if test "$enable_framework"; then + LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)' + fi fi ;; OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";; @@ -6761,8 +6767,8 @@ if test "$PY_ENABLE_SHARED" = "1" && ( test -n "$ANDROID_API_LEVEL" || test "$MA LIBPYTHON="-lpython${VERSION}${ABIFLAGS}" fi -# On iOS the shared libraries must be linked with the Python framework -if test "$ac_sys_system" = "iOS"; then +# On iOS the shared libraries must be linked with the framework, when built +if test "$ac_sys_system" = "iOS" && test "$enable_framework"; then MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)" fi @@ -8391,6 +8397,15 @@ AS_CASE([$host_cpu], ) AC_SUBST([MODULE_BUILDTYPE]) +dnl A non-framework iOS build can neither link nor load shared extension +dnl modules; some test modules can only be shared (see Modules/Setup.stdlib.in). +AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], [ + AS_IF([test "$MODULE_BUILDTYPE" != "static"], + [AC_MSG_ERROR([iOS builds must use --enable-framework; a non-framework build cannot build shared extension modules, and requires MODULE_BUILDTYPE=static])]) + AS_IF([test "$TEST_MODULES" != "no"], + [AC_MSG_ERROR([iOS builds must use --enable-framework; a non-framework build cannot build the shared test modules, and requires --disable-test-modules])]) +]) + dnl _MODULE_BLOCK_ADD([VAR], [VALUE]) dnl internal: adds $1=quote($2) to MODULE_BLOCK AC_DEFUN([_MODULE_BLOCK_ADD], [AS_VAR_APPEND([MODULE_BLOCK], ["$1=_AS_QUOTE([$2])$as_nl"])])