diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst index 8b4940ceb9521a7..88b5f35a7967967 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 31d9e2f2c816e73..80b9b3c3a07f063 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 000000000000000..4b8c1c197ddf5db --- /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 faeeead1df03a2e..fcf7d7f6fd437f6 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,72 @@ 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. Neither option therefore +needs to be given. + +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 \ + --prefix=/path/to/install/dir \ + --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`. +Unlike a framework build, `--prefix` is not set for you, so specify it +explicitly - otherwise `libpython` will be installed into `/usr/local`. + +#### 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 6b560fe6841b722..bf0a725a25208c5 100755 --- a/configure +++ b/configure @@ -4441,29 +4441,25 @@ 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 - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - - if test "x${prefix}" = "xNONE"; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + + if test "x${prefix}" = "xNONE"; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; *) PYTHONFRAMEWORKPREFIX="${enableval}" @@ -4558,28 +4554,24 @@ 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 - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - if test "x${prefix}" = "xNONE" ; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + if test "x${prefix}" = "xNONE" ; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; esac fi @@ -8027,6 +8019,9 @@ printf "%s\n" "#define Py_ENABLE_SHARED 1" >>confdefs.h RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ;; iOS) + if test -z "$PYTHONFRAMEWORK"; then + as_fn_error $? "iOS shared builds must use --enable-framework; an iOS app can only load a signed framework, never a bare dylib" "$LINENO" 5 + fi LDLIBRARY='libpython$(LDVERSION).dylib' ;; AIX*) @@ -14540,7 +14535,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 +28219,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 @@ -35043,6 +35042,13 @@ fi { printf "%s\n" "$as_me:${as_lineno-$LINENO}: result: $TEST_MODULES" >&5 printf "%s\n" "$TEST_MODULES" >&6; } +if test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK" +then : + if test "$TEST_MODULES" != "no" +then : + as_fn_error $? "iOS non-framework builds must use --disable-test-modules" "$LINENO" 5 +fi +fi # Check for --with-build-details-suffix @@ -35333,6 +35339,14 @@ 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 non-framework builds must use MODULE_BUILDTYPE=static" "$LINENO" 5 +fi +fi + MODULE_BLOCK= diff --git a/configure.ac b/configure.ac index 476f13c82bbb2b9..6d3af0e46354d40 100644 --- a/configure.ac +++ b/configure.ac @@ -578,29 +578,25 @@ 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 - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - - if test "x${prefix}" = "xNONE"; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + + if test "x${prefix}" = "xNONE"; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ;; *) PYTHONFRAMEWORKPREFIX="${enableval}" @@ -688,28 +684,24 @@ AC_ARG_ENABLE([framework], esac esac ],[ - case $ac_sys_system in - iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;; - *) - PYTHONFRAMEWORK= - PYTHONFRAMEWORKDIR=no-framework - PYTHONFRAMEWORKPREFIX= - PYTHONFRAMEWORKINSTALLDIR= - PYTHONFRAMEWORKINSTALLNAMEPREFIX= - RESSRCDIR= - FRAMEWORKINSTALLFIRST= - FRAMEWORKINSTALLLAST= - FRAMEWORKALTINSTALLFIRST= - FRAMEWORKALTINSTALLLAST= - FRAMEWORKPYTHONW= - INSTALLTARGETS="commoninstall bininstall maninstall" - if test "x${prefix}" = "xNONE" ; then - FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" - else - FRAMEWORKUNIXTOOLSPREFIX="${prefix}" - fi - enable_framework= - esac + PYTHONFRAMEWORK= + PYTHONFRAMEWORKDIR=no-framework + PYTHONFRAMEWORKPREFIX= + PYTHONFRAMEWORKINSTALLDIR= + PYTHONFRAMEWORKINSTALLNAMEPREFIX= + RESSRCDIR= + FRAMEWORKINSTALLFIRST= + FRAMEWORKINSTALLLAST= + FRAMEWORKALTINSTALLFIRST= + FRAMEWORKALTINSTALLLAST= + FRAMEWORKPYTHONW= + INSTALLTARGETS="commoninstall bininstall maninstall" + if test "x${prefix}" = "xNONE" ; then + FRAMEWORKUNIXTOOLSPREFIX="${ac_default_prefix}" + else + FRAMEWORKUNIXTOOLSPREFIX="${prefix}" + fi + enable_framework= ]) AC_SUBST([PYTHONFRAMEWORK]) AC_SUBST([PYTHONFRAMEWORKIDENTIFIER]) @@ -1676,6 +1668,9 @@ if test $enable_shared = "yes"; then RUNSHARED=DYLD_LIBRARY_PATH=`pwd`${DYLD_LIBRARY_PATH:+:${DYLD_LIBRARY_PATH}} ;; iOS) + if test -z "$PYTHONFRAMEWORK"; then + AC_MSG_ERROR([iOS shared builds must use --enable-framework; an iOS app can only load a signed framework, never a bare dylib]) + fi LDLIBRARY='libpython$(LDVERSION).dylib' ;; AIX*) @@ -3836,7 +3831,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 +6760,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 @@ -8152,6 +8151,11 @@ AC_ARG_ENABLE([test-modules], AS_VAR_IF([enable_test_modules], [yes], [TEST_MODULES=yes], [TEST_MODULES=no]) ], [TEST_MODULES=yes]) AC_MSG_RESULT([$TEST_MODULES]) +dnl Some test modules can only be built as shared libraries (see +dnl Modules/Setup.stdlib.in), which a non-framework iOS build cannot do. +AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], + [AS_IF([test "$TEST_MODULES" != "no"], + [AC_MSG_ERROR([iOS non-framework builds must use --disable-test-modules])])]) AC_SUBST([TEST_MODULES]) # Check for --with-build-details-suffix @@ -8391,6 +8395,12 @@ AS_CASE([$host_cpu], ) AC_SUBST([MODULE_BUILDTYPE]) +dnl A non-framework iOS build has no framework for a shared extension module +dnl to link against, so every extension module must be built into libpython. +AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], + [AS_IF([test "$MODULE_BUILDTYPE" != "static"], + [AC_MSG_ERROR([iOS non-framework builds must use MODULE_BUILDTYPE=static])])]) + 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"])])