Skip to content

Commit 14b7fc3

Browse files
committed
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. 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 each of them explicitly. On top of --disable-framework and --disable-shared, configure 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). Passing no framework option at all still errors, so a non-framework build stays an explicit opt-in. 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.
1 parent 98bd716 commit 14b7fc3

6 files changed

Lines changed: 158 additions & 15 deletions

File tree

‎Doc/using/configure.rst‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1338,6 +1338,21 @@ See :source:`Platforms/Apple/iOS/README.md`.
13381338
Create a Python.framework. Unlike macOS, the *INSTALLDIR* argument
13391339
specifying the installation path is mandatory.
13401340

1341+
.. option:: --disable-framework
1342+
1343+
Build a static ``libpython`` for embedding directly in an app binary,
1344+
instead of a ``Python.framework``. An iOS build that specifies neither
1345+
``--enable-framework`` nor ``--disable-framework`` is an error.
1346+
1347+
A static build cannot load extension modules at runtime, and so does not
1348+
support binary wheels. Every restriction that follows from this must be
1349+
opted into explicitly: ``--disable-framework`` also requires
1350+
``--disable-shared``, ``MODULE_BUILDTYPE=static`` and
1351+
:option:`--disable-test-modules`. See
1352+
:source:`Platforms/Apple/iOS/README.md` for the full list of limitations.
1353+
1354+
.. versionadded:: 3.16
1355+
13411356
.. option:: --with-framework-name=FRAMEWORK
13421357

13431358
Specify the name for the framework (default: ``Python``).

‎Doc/using/ios.rst‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,13 @@ should ensure these stub binaries are on your path.
142142
Installing Python on iOS
143143
========================
144144

145+
The official iOS release artefact is a framework build, distributed as an
146+
``XCFramework``; this is the configuration described in the rest of this
147+
document, and the only one that supports binary extension modules. Static
148+
builds, where ``libpython`` and every extension module are linked directly into
149+
the app binary, are also possible, with limitations; see
150+
:source:`Platforms/Apple/iOS/README.md` for details.
151+
145152
Tools for building iOS apps
146153
---------------------------
147154

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
iOS builds may now be configured with ``--disable-framework``, producing a
2+
static ``libpython`` for embedding in an app binary. This configuration cannot
3+
load extension modules at runtime, and requires ``--disable-shared``,
4+
``MODULE_BUILDTYPE=static`` and ``--disable-test-modules`` to be requested
5+
explicitly. A shared iOS build must still be a framework build.

‎Platforms/Apple/iOS/README.md‎

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,8 +90,15 @@ Python build for a single framework, the following options are available.
9090
installed. If `DIR` is not specified, the framework will be installed into
9191
a subdirectory of the `iOS/Frameworks` folder.
9292

93-
This argument *must* be provided when configuring iOS builds. iOS does not
94-
support non-framework builds.
93+
Either this argument or `--disable-framework` *must* be provided when
94+
configuring iOS builds; there is no default.
95+
96+
* `--disable-framework`
97+
98+
Build a static `libpython` for embedding directly in an app binary, instead
99+
of a `Python.framework`. This configuration comes with significant
100+
restrictions, each of which must be opted into explicitly; see [Building a
101+
static Python](#building-a-static-python) below.
95102

96103
* `--with-framework-name=NAME`
97104

@@ -113,9 +120,11 @@ framework to contain non-library content, so the iOS build will produce a
113120
The `lib` folder will be needed at runtime to support the Python library.
114121

115122
If you want to use Python in a real iOS project, you need to produce multiple
116-
`Python.framework` builds, one for each ABI and architecture. iOS builds of
117-
Python *must* be constructed as framework builds. To support this, you must
118-
provide the `--enable-framework` flag when configuring the build. The build
123+
`Python.framework` builds, one for each ABI and architecture. Unless you are
124+
statically linking Python into your app (see [Building a static
125+
Python](#building-a-static-python) below), iOS builds of Python *must* be
126+
constructed as framework builds. To support this, you must provide the
127+
`--enable-framework` flag when configuring the build. The build
119128
also requires the use of cross-compilation. The minimal commands for building
120129
Python for the ARM64 iOS simulator will look something like:
121130
```
@@ -216,6 +225,70 @@ target, provide the version number as part of the `--host` argument - for
216225
example, `--host=arm64-apple-ios15.4-simulator` would compile an ARM64
217226
simulator build with a deployment target of 15.4.
218227

228+
### Building a static Python
229+
230+
The official iOS release artefact is a framework build. However, if you are
231+
embedding Python in an app that links `libpython` at compile time, you can
232+
instead build a static `libpython3.x.a`, and link that archive directly into
233+
your app binary.
234+
235+
The App Store requirement that binary modules be packaged as signed frameworks
236+
does not apply to a static build, because a static build loads nothing at
237+
runtime; but for the same reason, this configuration cannot use *any* binary
238+
module that isn't compiled into the app binary. Every restriction that follows
239+
from that must be opted into explicitly at configure time:
240+
241+
* `--disable-framework` selects a non-framework build. It is an error to
242+
provide neither `--enable-framework` nor `--disable-framework`.
243+
244+
* `--disable-shared` is required. A shared iOS build must be a framework
245+
build, as an iOS app can only load a signed framework, never a bare dylib.
246+
247+
* `MODULE_BUILDTYPE=static` is required. There is no framework for a shared
248+
extension module to link against, so every extension module - including the
249+
ones in the standard library - must be linked into `libpython`.
250+
251+
* `--disable-test-modules` is required. Some test modules must be compiled as
252+
shared libraries (see `Modules/Setup.stdlib.in`), so they cannot be built in
253+
this configuration at all.
254+
255+
The minimal commands for a static build targeting ARM64 iOS devices are then:
256+
```
257+
export PATH="$(pwd)/Platforms/Apple/iOS/Resources/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Library/Apple/usr/bin"
258+
./configure \
259+
--disable-framework \
260+
--disable-shared \
261+
--disable-test-modules \
262+
MODULE_BUILDTYPE=static \
263+
--host=arm64-apple-ios \
264+
--build=arm64-apple-darwin \
265+
--with-build-python=/path/to/python.exe
266+
make
267+
make install
268+
```
269+
This produces a `libpython3.x.a` containing the interpreter and the standard
270+
library's extension modules; `make install` installs that archive, along with
271+
the standard library's Python source, into the location given by `--prefix`.
272+
273+
#### Limitations of a static build
274+
275+
* **Binary wheels cannot be used.** There is no `libpython` dylib for a
276+
third-party extension module to link against, and a static Python has nothing
277+
to `dlopen` in any case. Pure Python wheels work as normal; any package with a
278+
C extension must be compiled into the app binary alongside `libpython`.
279+
280+
* **The standard library's extension modules are not loadable modules.** They
281+
live in the archive, not in `.framework` bundles in the app's `Frameworks`
282+
folder, so the packaging described in
283+
[Using Python on iOS](https://docs.python.org/3/using/ios.html) does not apply
284+
to them.
285+
286+
* **The test suite cannot be run as-is**, as the test modules are not built.
287+
288+
* This configuration is not covered by the `Platforms/Apple` build script, nor
289+
by CPython's CI. It is not the configuration used to produce official
290+
releases.
291+
219292
## Testing Python on iOS
220293

221294
### Testing a multi-architecture framework

‎configure‎

Lines changed: 27 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎configure.ac‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -579,7 +579,6 @@ AC_ARG_ENABLE([framework],
579579
case $enableval in
580580
no)
581581
case $ac_sys_system in
582-
iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;;
583582
*)
584583
PYTHONFRAMEWORK=
585584
PYTHONFRAMEWORKDIR=no-framework
@@ -689,7 +688,7 @@ AC_ARG_ENABLE([framework],
689688
esac
690689
],[
691690
case $ac_sys_system in
692-
iOS) AC_MSG_ERROR([iOS builds must use --enable-framework]) ;;
691+
iOS) AC_MSG_ERROR([iOS builds must use --enable-framework, or --disable-framework with --disable-shared to build a static libpython]) ;;
693692
*)
694693
PYTHONFRAMEWORK=
695694
PYTHONFRAMEWORKDIR=no-framework
@@ -1695,6 +1694,10 @@ else # shared is disabled
16951694
fi
16961695
AC_MSG_RESULT([$LDLIBRARY])
16971696

1697+
if test "$ac_sys_system" = "iOS" && test "$PY_ENABLE_SHARED" = 1 && test -z "$PYTHONFRAMEWORK"; then
1698+
AC_MSG_ERROR([iOS builds must use --enable-framework, or --disable-framework with --disable-shared to build a static libpython])
1699+
fi
1700+
16981701
# HOSTRUNNER - Program to run CPython for the host platform
16991702
AC_MSG_CHECKING([HOSTRUNNER])
17001703
if test -z "$HOSTRUNNER"
@@ -3836,7 +3839,11 @@ then
38363839
fi
38373840
LINKFORSHARED="$LINKFORSHARED"
38383841
elif test $ac_sys_system = "iOS"; then
3839-
LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)'
3842+
LINKFORSHARED="-Wl,-stack_size,$stack_size $LINKFORSHARED"
3843+
3844+
if test "$enable_framework"; then
3845+
LINKFORSHARED="$LINKFORSHARED "'$(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK)'
3846+
fi
38403847
fi
38413848
;;
38423849
OpenUNIX*|UnixWare*) LINKFORSHARED="-Wl,-Bexport";;
@@ -6761,8 +6768,9 @@ if test "$PY_ENABLE_SHARED" = "1" && ( test -n "$ANDROID_API_LEVEL" || test "$MA
67616768
LIBPYTHON="-lpython${VERSION}${ABIFLAGS}"
67626769
fi
67636770

6764-
# On iOS the shared libraries must be linked with the Python framework
6765-
if test "$ac_sys_system" = "iOS"; then
6771+
# On iOS the shared libraries must be linked with the Python framework, when one
6772+
# is being built
6773+
if test "$ac_sys_system" = "iOS" && test "$enable_framework"; then
67666774
MODULE_DEPS_SHARED="$MODULE_DEPS_SHARED \$(PYTHONFRAMEWORKDIR)/\$(PYTHONFRAMEWORK)"
67676775
fi
67686776

@@ -8391,6 +8399,19 @@ AS_CASE([$host_cpu],
83918399
)
83928400
AC_SUBST([MODULE_BUILDTYPE])
83938401

8402+
dnl A non-framework iOS build is statically linked into the app binary. It has
8403+
dnl no framework for extension modules to link against, and cannot load them at
8404+
dnl runtime, so every extension module must be linked into libpython, and the
8405+
dnl test modules that must be built as shared libraries cannot be built at all
8406+
dnl (see Modules/Setup.stdlib.in). Each of those restrictions must be opted
8407+
dnl into explicitly.
8408+
AS_IF([test "$ac_sys_system" = "iOS" && test -z "$PYTHONFRAMEWORK"], [
8409+
AS_IF([test "$MODULE_BUILDTYPE" != "static"],
8410+
[AC_MSG_ERROR([non-framework iOS builds cannot build shared extension modules; configure with MODULE_BUILDTYPE=static])])
8411+
AS_IF([test "$TEST_MODULES" != "no"],
8412+
[AC_MSG_ERROR([non-framework iOS builds cannot build the shared test modules; configure with --disable-test-modules])])
8413+
])
8414+
83948415
dnl _MODULE_BLOCK_ADD([VAR], [VALUE])
83958416
dnl internal: adds $1=quote($2) to MODULE_BLOCK
83968417
AC_DEFUN([_MODULE_BLOCK_ADD], [AS_VAR_APPEND([MODULE_BLOCK], ["$1=_AS_QUOTE([$2])$as_nl"])])

0 commit comments

Comments
 (0)