Skip to content

Commit 9ea7af1

Browse files
freakboy3742mhsmithericsnowcurrently
committed
[3.11] pythongh-114099 - Add iOS framework loading machinery. (pythonGH-116454)
Co-authored-by: Malcolm Smith <smith@chaquo.com> Co-authored-by: Eric Snow <ericsnowcurrently@gmail.com>
1 parent 99e7ae4 commit 9ea7af1

File tree

21 files changed

+217
-35
lines changed

21 files changed

+217
-35
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ Lib/test/data/*
6565
/_bootstrap_python
6666
/Makefile
6767
/Makefile.pre
68-
iOSTestbed.*
68+
/iOSTestbed.*
6969
iOS/Frameworks/
7070
iOS/Resources/Info.plist
7171
iOS/testbed/build

Doc/library/importlib.rst

+63
Original file line numberDiff line numberDiff line change
@@ -1202,6 +1202,69 @@ find and load modules.
12021202
and how the module's :attr:`__file__` is populated.
12031203

12041204

1205+
.. class:: AppleFrameworkLoader(name, path)
1206+
1207+
A specialization of :class:`importlib.machinery.ExtensionFileLoader` that
1208+
is able to load extension modules in Framework format.
1209+
1210+
For compatibility with the iOS App Store, *all* binary modules in an iOS app
1211+
must be dynamic libraries, contained in a framework with appropriate
1212+
metadata, stored in the ``Frameworks`` folder of the packaged app. There can
1213+
be only a single binary per framework, and there can be no executable binary
1214+
material outside the Frameworks folder.
1215+
1216+
To accomodate this requirement, when running on iOS, extension module
1217+
binaries are *not* packaged as ``.so`` files on ``sys.path``, but as
1218+
individual standalone frameworks. To discover those frameworks, this loader
1219+
is be registered against the ``.fwork`` file extension, with a ``.fwork``
1220+
file acting as a placeholder in the original location of the binary on
1221+
``sys.path``. The ``.fwork`` file contains the path of the actual binary in
1222+
the ``Frameworks`` folder, relative to the app bundle. To allow for
1223+
resolving a framework-packaged binary back to the original location, the
1224+
framework is expected to contain a ``.origin`` file that contains the
1225+
location of the ``.fwork`` file, relative to the app bundle.
1226+
1227+
For example, consider the case of an import ``from foo.bar import _whiz``,
1228+
where ``_whiz`` is implemented with the binary module
1229+
``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location
1230+
registered on ``sys.path``, relative to the application bundle. This module
1231+
*must* be distributed as
1232+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` (creating the framework
1233+
name from the full import path of the module), with an ``Info.plist`` file
1234+
in the ``.framework`` directory identifying the binary as a framework. The
1235+
``foo.bar._whiz`` module would be represented in the original location with
1236+
a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing the path
1237+
``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also contain
1238+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing the
1239+
path to the ``.fwork`` file.
1240+
1241+
When a module is loaded with this loader, the ``__file__`` for the module
1242+
will report as the location of the ``.fwork`` file. This allows code to use
1243+
the ``__file__`` of a module as an anchor for file system traveral.
1244+
However, the spec origin will reference the location of the *actual* binary
1245+
in the ``.framework`` folder.
1246+
1247+
The Xcode project building the app is responsible for converting any ``.so``
1248+
files from wherever they exist in the ``PYTHONPATH`` into frameworks in the
1249+
``Frameworks`` folder (including stripping extensions from the module file,
1250+
the addition of framework metadata, and signing the resulting framework),
1251+
and creating the ``.fwork`` and ``.origin`` files. This will usually be done
1252+
with a build step in the Xcode project; see the iOS documentation for
1253+
details on how to construct this build step.
1254+
1255+
.. versionadded:: 3.13
1256+
1257+
.. availability:: iOS.
1258+
1259+
.. attribute:: name
1260+
1261+
Name of the module the loader supports.
1262+
1263+
.. attribute:: path
1264+
1265+
Path to the ``.fwork`` file for the extension module.
1266+
1267+
12051268
:mod:`importlib.util` -- Utility code for importers
12061269
---------------------------------------------------
12071270

Doc/tools/extensions/pyspecific.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ class Availability(SphinxDirective):
138138
known_platforms = frozenset({
139139
"AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD",
140140
"Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris", "Unix", "VxWorks",
141-
"WASI", "Windows", "macOS",
141+
"WASI", "Windows", "macOS", "iOS",
142142
# libc
143143
"BSD libc", "glibc", "musl",
144144
# POSIX platforms with pthreads

Lib/ctypes/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,19 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None,
343343
use_errno=False,
344344
use_last_error=False,
345345
winmode=None):
346+
if name:
347+
name = _os.fspath(name)
348+
349+
# If the filename that has been provided is an iOS/tvOS/watchOS
350+
# .fwork file, dereference the location to the true origin of the
351+
# binary.
352+
if name.endswith(".fwork"):
353+
with open(name) as f:
354+
name = _os.path.join(
355+
_os.path.dirname(_sys.executable),
356+
f.read().strip()
357+
)
358+
346359
self._name = name
347360
flags = self._func_flags_
348361
if use_errno:

Lib/ctypes/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def find_library(name):
6767
return fname
6868
return None
6969

70-
elif os.name == "posix" and sys.platform == "darwin":
70+
elif os.name == "posix" and sys.platform in {"darwin", "ios", "tvos", "watchos"}:
7171
from ctypes.macholib.dyld import dyld_find as _dyld_find
7272
def find_library(name):
7373
possible = ['lib%s.dylib' % name,

Lib/importlib/_bootstrap_external.py

+50-3
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252

5353
# Bootstrap-related code ######################################################
5454
_CASE_INSENSITIVE_PLATFORMS_STR_KEY = 'win',
55-
_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin'
55+
_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin', 'ios', 'tvos', 'watchos'
5656
_CASE_INSENSITIVE_PLATFORMS = (_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY
5757
+ _CASE_INSENSITIVE_PLATFORMS_STR_KEY)
5858

@@ -1708,6 +1708,46 @@ def __repr__(self):
17081708
return 'FileFinder({!r})'.format(self.path)
17091709

17101710

1711+
class AppleFrameworkLoader(ExtensionFileLoader):
1712+
"""A loader for modules that have been packaged as frameworks for
1713+
compatibility with Apple's iOS App Store policies.
1714+
"""
1715+
def create_module(self, spec):
1716+
# If the ModuleSpec has been created by the FileFinder, it will have
1717+
# been created with an origin pointing to the .fwork file. We need to
1718+
# redirect this to the location in the Frameworks folder, using the
1719+
# content of the .fwork file.
1720+
if spec.origin.endswith(".fwork"):
1721+
with _io.FileIO(spec.origin, 'r') as file:
1722+
framework_binary = file.read().decode().strip()
1723+
bundle_path = _path_split(sys.executable)[0]
1724+
spec.origin = _path_join(bundle_path, framework_binary)
1725+
1726+
# If the loader is created based on the spec for a loaded module, the
1727+
# path will be pointing at the Framework location. If this occurs,
1728+
# get the original .fwork location to use as the module's __file__.
1729+
if self.path.endswith(".fwork"):
1730+
path = self.path
1731+
else:
1732+
with _io.FileIO(self.path + ".origin", 'r') as file:
1733+
origin = file.read().decode().strip()
1734+
bundle_path = _path_split(sys.executable)[0]
1735+
path = _path_join(bundle_path, origin)
1736+
1737+
module = _bootstrap._call_with_frames_removed(_imp.create_dynamic, spec)
1738+
1739+
_bootstrap._verbose_message(
1740+
"Apple framework extension module {!r} loaded from {!r} (path {!r})",
1741+
spec.name,
1742+
spec.origin,
1743+
path,
1744+
)
1745+
1746+
# Ensure that the __file__ points at the .fwork location
1747+
module.__file__ = path
1748+
1749+
return module
1750+
17111751
# Import setup ###############################################################
17121752

17131753
def _fix_up_module(ns, name, pathname, cpathname=None):
@@ -1738,10 +1778,17 @@ def _get_supported_file_loaders():
17381778
17391779
Each item is a tuple (loader, suffixes).
17401780
"""
1741-
extensions = ExtensionFileLoader, _imp.extension_suffixes()
1781+
if sys.platform in {"ios", "tvos", "watchos"}:
1782+
extension_loaders = [(AppleFrameworkLoader, [
1783+
suffix.replace(".so", ".fwork")
1784+
for suffix in _imp.extension_suffixes()
1785+
])]
1786+
else:
1787+
extension_loaders = []
1788+
extension_loaders.append((ExtensionFileLoader, _imp.extension_suffixes()))
17421789
source = SourceFileLoader, SOURCE_SUFFIXES
17431790
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
1744-
return [extensions, source, bytecode]
1791+
return extension_loaders + [source, bytecode]
17451792

17461793

17471794
def _set_bootstrap_module(_bootstrap_module):

Lib/importlib/abc.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,11 @@ def get_code(self, fullname):
261261
else:
262262
return self.source_to_code(source, path)
263263

264-
_register(ExecutionLoader, machinery.ExtensionFileLoader)
264+
_register(
265+
ExecutionLoader,
266+
machinery.ExtensionFileLoader,
267+
machinery.AppleFrameworkLoader,
268+
)
265269

266270

267271
class FileLoader(_bootstrap_external.FileLoader, ResourceLoader, ExecutionLoader):

Lib/importlib/machinery.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._bootstrap_external import SourceFileLoader
1313
from ._bootstrap_external import SourcelessFileLoader
1414
from ._bootstrap_external import ExtensionFileLoader
15+
from ._bootstrap_external import AppleFrameworkLoader
1516
from ._bootstrap_external import NamespaceLoader
1617

1718

Lib/inspect.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -972,6 +972,7 @@ def getmodule(object, _filename=None):
972972
return object
973973
if hasattr(object, '__module__'):
974974
return sys.modules.get(object.__module__)
975+
975976
# Try the filename to modulename cache
976977
if _filename is not None and _filename in modulesbyfile:
977978
return sys.modules.get(modulesbyfile[_filename])
@@ -1065,7 +1066,7 @@ def findsource(object):
10651066
# Allow filenames in form of "<something>" to pass through.
10661067
# `doctest` monkeypatches `linecache` module to enable
10671068
# inspection, so let `linecache.getlines` to be called.
1068-
if not (file.startswith('<') and file.endswith('>')):
1069+
if (not (file.startswith('<') and file.endswith('>'))) or file.endswith('.fwork'):
10691070
raise OSError('source code not available')
10701071

10711072
module = getmodule(object, file)

Lib/modulefinder.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ def _find_module(name, path=None):
7272
if isinstance(spec.loader, importlib.machinery.SourceFileLoader):
7373
kind = _PY_SOURCE
7474

75-
elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader):
75+
elif isinstance(
76+
spec.loader, (
77+
importlib.machinery.ExtensionFileLoader,
78+
importlib.machinery.AppleFrameworkLoader,
79+
)
80+
):
7681
kind = _C_EXTENSION
7782

7883
elif isinstance(spec.loader, importlib.machinery.SourcelessFileLoader):

Lib/test/test_capi/test_misc.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -1099,14 +1099,21 @@ def test_module_state_shared_in_global(self):
10991099
self.addCleanup(os.close, r)
11001100
self.addCleanup(os.close, w)
11011101

1102+
# Apple extensions must be distributed as frameworks. This requires
1103+
# a specialist loader.
1104+
if support.is_apple_mobile:
1105+
loader = "AppleFrameworkLoader"
1106+
else:
1107+
loader = "ExtensionFileLoader"
1108+
11021109
script = textwrap.dedent(f"""
11031110
import importlib.machinery
11041111
import importlib.util
11051112
import os
11061113
11071114
fullname = '_test_module_state_shared'
11081115
origin = importlib.util.find_spec('_testmultiphase').origin
1109-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
1116+
loader = importlib.machinery.{loader}(fullname, origin)
11101117
spec = importlib.util.spec_from_loader(fullname, loader)
11111118
module = importlib.util.module_from_spec(spec)
11121119
attr_id = str(id(module.Error)).encode()
@@ -1311,7 +1318,12 @@ class Test_ModuleStateAccess(unittest.TestCase):
13111318
def setUp(self):
13121319
fullname = '_testmultiphase_meth_state_access' # XXX
13131320
origin = importlib.util.find_spec('_testmultiphase').origin
1314-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
1321+
# Apple extensions must be distributed as frameworks. This requires
1322+
# a specialist loader.
1323+
if support.is_apple_mobile:
1324+
loader = importlib.machinery.AppleFrameworkLoader(fullname, origin)
1325+
else:
1326+
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
13151327
spec = importlib.util.spec_from_loader(fullname, loader)
13161328
module = importlib.util.module_from_spec(spec)
13171329
loader.exec_module(module)

Lib/test/test_import/__init__.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020

2121
from test.support import os_helper
2222
from test.support import (
23-
STDLIB_DIR, is_jython, swap_attr, swap_item, cpython_only, is_emscripten,
23+
STDLIB_DIR, is_jython, swap_attr, swap_item, cpython_only, is_apple_mobile, is_emscripten,
2424
is_wasi)
2525
from test.support.import_helper import (
2626
forget, make_legacy_pyc, unlink, unload, ready_to_import,
@@ -86,7 +86,7 @@ def test_from_import_missing_attr_has_name_and_so_path(self):
8686
self.assertEqual(cm.exception.path, _testcapi.__file__)
8787
self.assertRegex(
8888
str(cm.exception),
89-
r"cannot import name 'i_dont_exist' from '_testcapi' \(.*\.(so|pyd)\)"
89+
r"cannot import name 'i_dont_exist' from '_testcapi' \(.*\.(so|fwork|pyd)\)"
9090
)
9191
else:
9292
self.assertEqual(

Lib/test/test_importlib/extension/test_finder.py

+22-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from test.support import is_apple_mobile
12
from test.test_importlib import abc, util
23

34
machinery = util.import_importlib('importlib.machinery')
@@ -19,9 +20,27 @@ def setUp(self):
1920
)
2021

2122
def find_spec(self, fullname):
22-
importer = self.machinery.FileFinder(util.EXTENSIONS.path,
23-
(self.machinery.ExtensionFileLoader,
24-
self.machinery.EXTENSION_SUFFIXES))
23+
if is_apple_mobile:
24+
# Apple mobile platforms require a specialist loader that uses
25+
# .fwork files as placeholders for the true `.so` files.
26+
loaders = [
27+
(
28+
self.machinery.AppleFrameworkLoader,
29+
[
30+
ext.replace(".so", ".fwork")
31+
for ext in self.machinery.EXTENSION_SUFFIXES
32+
]
33+
)
34+
]
35+
else:
36+
loaders = [
37+
(
38+
self.machinery.ExtensionFileLoader,
39+
self.machinery.EXTENSION_SUFFIXES
40+
)
41+
]
42+
43+
importer = self.machinery.FileFinder(util.EXTENSIONS.path, *loaders)
2544

2645
return importer.find_spec(fullname)
2746

0 commit comments

Comments
 (0)