Skip to content

gh-156109: Allow static, non-framework iOS builds - #156110

Open
clementperon wants to merge 1 commit into
python:mainfrom
clementperon:ios-static-nonframework
Open

gh-156109: Allow static, non-framework iOS builds#156110
clementperon wants to merge 1 commit into
python:mainfrom
clementperon:ios-static-nonframework

Conversation

@clementperon

@clementperon clementperon commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

configure refuses any non-framework iOS build. That is correct for a shared Python — an iOS app can only load a signed framework, never a bare dylib — but a --disable-shared build produces a libpython3.x.a that is linked into the app binary and loads nothing at runtime, so the requirement does not apply to it.

This narrows the refusal to the combination that genuinely cannot work, and stops two Makefile variables referencing a framework that a non-framework build never produces.

  • The explicit --disable-framework path no longer errors on iOS; it falls through to the existing non-framework setup.
  • A new check, placed once PY_ENABLE_SHARED is known, rejects a shared build with no framework.
  • LINKFORSHARED and MODULE_DEPS_SHARED gate their $(PYTHONFRAMEWORKDIR)/$(PYTHONFRAMEWORK) appends on enable_framework, as the Darwin arm directly above the first one already does.
  • Passing no framework option at all still errors, so a non-framework build stays an explicit opt-in. Its message now names --disable-framework.

Verified on macOS/arm64 against main:

Configure (--host=arm64-apple-ios12.0) Result
--disable-framework --disable-shared configures; make libpython3.16.a succeeds — arm64, LC_BUILD_VERSION platform 2, minos 12.0
--disable-framework --enable-shared rejected: iOS builds must use --enable-framework, or --disable-shared to build a static libpython
no framework option rejected: iOS builds must use --enable-framework, or --disable-framework to build a static libpython
--enable-framework Makefile and pyconfig.h byte-identical to unpatched main

configure was regenerated with Tools/build/regen-configure.sh; autoreconf -ivf -Werror is clean and the generated diff contains no unrelated churn.

@clementperon
clementperon force-pushed the ios-static-nonframework branch from 82788a4 to e710088 Compare August 20, 2026 13:37
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
clementperon added a commit to clementperon/xbmc that referenced this pull request Aug 20, 2026
CPython appends -mios-version-min - the device flag - to every iOS build.
Against the simulator SDK the linker rejects the dylibs it resolves there, and
the first configure check that links one fails, taking the depends build with
it.

The local patch faked ac_sys_system=iOS by deleting CPython's host parsing,
because config.site.in pinned every package to the tree's darwin triplet. Python
now configures against its own iOS triplet, so CPython derives ac_sys_system,
the deployment target and the device/simulator split itself, and the 76 lines
that deleted that logic are gone.

What is left matches three patches under review upstream, so all three can be
dropped when python is next bumped:

  python/cpython#156110
  python/cpython#156116
  python/cpython#156113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@freakboy3742

Copy link
Copy Markdown
Contributor

From a pure configuration perspective, this seems a fairly straightforward.

My concern is the impact on wheels (and, for that matter, the binaries in the standard library). iOS wheels are linked against libpython because -undefined is flagged as a deprecated linker argument.

What (if any) testing have you done of this code with the standard library and/or third party libraries?

@clementperon

clementperon commented Aug 21, 2026

Copy link
Copy Markdown
Contributor Author

Context first: Kodi embeds Python in a single signed app binary. It has built
with --disable-shared --disable-framework since 2017, and carries a
downstream patch to get past the iOS refusal — so this PR asks upstream to
allow what that patch already does, rather than to enable something new.

The stdlib is built static (MODULE_BUILDTYPE=static), so no wheels are
involved there. Binary addon modules are a different matter: xbmc/xbmc#28808
enables AppleFrameworkLoader so they can load from <app bundle>/Frameworks,
with packaging still in progress. So a static libpython and dynamically loaded
extensions do coexist in this configuration — how those extensions resolve
their Python symbols is the part I have not verified.

Testing, honestly: I built libpython3.16.a for --host=aarch64-apple-ios12.0 --disable-shared --disable-framework and checked the configure matrix. No
extension modules, no wheels. I did verify that --enable-framework produces a
byte-identical Makefile and pyconfig.h to unpatched main, so nothing
possible today changes.

Your point about wheels stands: with no libpython dylib there is nothing for a
prebuilt wheel to link against, and -undefined dynamic_lookup is deprecated.
What would you want configure to do about that:

  • document the mode as unsupported for wheels, or something stricter?

Happy to build the stdlib statically for iOS and report back if that helps.

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. Passing no framework option at all still
errors, so a non-framework build stays an explicit opt-in.
@clementperon
clementperon force-pushed the ios-static-nonframework branch from 995aa88 to fccc461 Compare August 21, 2026 15:03
@clementperon

Copy link
Copy Markdown
Contributor Author

Ran the stdlib build — you were right, this is broken as it stands.

make with --disable-shared --disable-framework fails on every shared extension module:

ld: library 'System' not found
make: *** [Modules/array.cpython-316.so] Error 1

configure.ac:3687 links iOS extensions against the framework unconditionally:

iOS/*)
    LDSHARED='$(CC) -dynamiclib -F . -framework $(PYTHONFRAMEWORK)'

PYTHONFRAMEWORK is empty without one, so -framework swallows the next argument. libpython3.16.a builds fine; only the shared modules fail.

So a static libpython needs static modules — Kodi passes MODULE_BUILDTYPE=static, which is why it never hit this. The alternatives are -undefined dynamic_lookup (deprecated) or -bundle_loader on the final app executable, which the build can't know.

I'll make configure require MODULE_BUILDTYPE=static for non-framework builds so this fails with a clear message instead, unless you'd rather it went differently.

@clementperon

Copy link
Copy Markdown
Contributor Author

Follow-up: with MODULE_BUILDTYPE=static and --disable-test-modules, make completes — 46MB libpython3.16.a, 269 objects, stdlib extensions inside it, no .so produced.

So my proposed requirement isn't sufficient alone: _testimportmultiple, xxlimited_35 and xxlimited_3_13 are shared by construction, so MODULE_BUILDTYPE doesn't reach them and they still fail on the empty -framework.

Either require --disable-test-modules too, or mark shared-only modules unavailable when there's no framework (like PY_STDLIB_MOD_SET_NA) so the flags aren't needed. The latter looks better to me — your call, happy to do either.

@freakboy3742

Copy link
Copy Markdown
Contributor

Ran the stdlib build — you were right, this is broken as it stands.
...
Either require --disable-test-modules too, or mark shared-only modules unavailable when there's no framework (like PY_STDLIB_MOD_SET_NA) so the flags aren't needed. The latter looks better to me — your call, happy to do either.

I feel like I'm having a conversation with an agent rather than a human, except without the ability to actually drive the conversation...

What exactly are you proposing as the options here?

@clementperon

Copy link
Copy Markdown
Contributor Author

@freakboy3742 yes, there is an AI agent in the loop. I use it to dig through the build system, and I read and approve everything before it gets posted.
But you were right about the shape of the conversation: posting each partial finding instead of coming back with one proposal. That's on me.

3 propositions:

  • same as WASM -> iOS with no framework, force MODULE_BUILDTYPE=static and mark the shared-only test modules N/A, so --disable-test-modules isn't needed and the docs state that neither loadable extensions nor binary wheels are supported (this make the changes a bit bigger)
  • no implicit behaviour: configure fails unless the user also passes MODULE_BUILDTYPE=static and --disable-test-modules
  • I close the PR and Kodi keeps the downstream patch

@freakboy3742

Copy link
Copy Markdown
Contributor
  • no implicit behaviour: configure fails unless the user also passes MODULE_BUILDTYPE=static and --disable-test-modules

My inclination is to go with this - the fully explicit option. A static build is clearly useful, but there will be restrictions on use - I think there is value in requiring the prospective user to explicitly opt into all those options.

These limitations will also need to be documented; a section on static builds (describing both the build process and limitations) to Platforms/Apple/iOS/README.md will be required. It may also be worth mentioning in Doc/using/iOS.rst that the official iOS release artefact is a framework build, but "static builds are also possible with limitations, see the README for details".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants