Skip to content

Commit 408e127

Browse files
freakboy3742mhsmithericsnowcurrently
authoredMar 19, 2024
pythongh-114099 - Add iOS framework loading machinery. (pythonGH-116454)
Co-authored-by: Malcolm Smith <[email protected]> Co-authored-by: Eric Snow <[email protected]>
1 parent a557478 commit 408e127

File tree

22 files changed

+302
-62
lines changed

22 files changed

+302
-62
lines changed
 

‎.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ Lib/test/data/*
6969
/_bootstrap_python
7070
/Makefile
7171
/Makefile.pre
72-
iOSTestbed.*
72+
/iOSTestbed.*
7373
iOS/Frameworks/
7474
iOS/Resources/Info.plist
7575
iOS/testbed/build

‎Doc/library/importlib.rst

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

12431243

1244+
.. class:: AppleFrameworkLoader(name, path)
1245+
1246+
A specialization of :class:`importlib.machinery.ExtensionFileLoader` that
1247+
is able to load extension modules in Framework format.
1248+
1249+
For compatibility with the iOS App Store, *all* binary modules in an iOS app
1250+
must be dynamic libraries, contained in a framework with appropriate
1251+
metadata, stored in the ``Frameworks`` folder of the packaged app. There can
1252+
be only a single binary per framework, and there can be no executable binary
1253+
material outside the Frameworks folder.
1254+
1255+
To accomodate this requirement, when running on iOS, extension module
1256+
binaries are *not* packaged as ``.so`` files on ``sys.path``, but as
1257+
individual standalone frameworks. To discover those frameworks, this loader
1258+
is be registered against the ``.fwork`` file extension, with a ``.fwork``
1259+
file acting as a placeholder in the original location of the binary on
1260+
``sys.path``. The ``.fwork`` file contains the path of the actual binary in
1261+
the ``Frameworks`` folder, relative to the app bundle. To allow for
1262+
resolving a framework-packaged binary back to the original location, the
1263+
framework is expected to contain a ``.origin`` file that contains the
1264+
location of the ``.fwork`` file, relative to the app bundle.
1265+
1266+
For example, consider the case of an import ``from foo.bar import _whiz``,
1267+
where ``_whiz`` is implemented with the binary module
1268+
``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location
1269+
registered on ``sys.path``, relative to the application bundle. This module
1270+
*must* be distributed as
1271+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` (creating the framework
1272+
name from the full import path of the module), with an ``Info.plist`` file
1273+
in the ``.framework`` directory identifying the binary as a framework. The
1274+
``foo.bar._whiz`` module would be represented in the original location with
1275+
a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing the path
1276+
``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also contain
1277+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing the
1278+
path to the ``.fwork`` file.
1279+
1280+
When a module is loaded with this loader, the ``__file__`` for the module
1281+
will report as the location of the ``.fwork`` file. This allows code to use
1282+
the ``__file__`` of a module as an anchor for file system traveral.
1283+
However, the spec origin will reference the location of the *actual* binary
1284+
in the ``.framework`` folder.
1285+
1286+
The Xcode project building the app is responsible for converting any ``.so``
1287+
files from wherever they exist in the ``PYTHONPATH`` into frameworks in the
1288+
``Frameworks`` folder (including stripping extensions from the module file,
1289+
the addition of framework metadata, and signing the resulting framework),
1290+
and creating the ``.fwork`` and ``.origin`` files. This will usually be done
1291+
with a build step in the Xcode project; see the iOS documentation for
1292+
details on how to construct this build step.
1293+
1294+
.. versionadded:: 3.13
1295+
1296+
.. availability:: iOS.
1297+
1298+
.. attribute:: name
1299+
1300+
Name of the module the loader supports.
1301+
1302+
.. attribute:: path
1303+
1304+
Path to the ``.fwork`` file for the extension module.
1305+
1306+
12441307
:mod:`importlib.util` -- Utility code for importers
12451308
---------------------------------------------------
12461309

‎Doc/tools/extensions/pyspecific.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ class Availability(SphinxDirective):
133133
known_platforms = frozenset({
134134
"AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD",
135135
"GNU/kFreeBSD", "Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris",
136-
"Unix", "VxWorks", "WASI", "Windows", "macOS",
136+
"Unix", "VxWorks", "WASI", "Windows", "macOS", "iOS",
137137
# libc
138138
"BSD libc", "glibc", "musl",
139139
# POSIX platforms with pthreads

‎Lib/ctypes/__init__.py

+11
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,17 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None,
348348
winmode=None):
349349
if name:
350350
name = _os.fspath(name)
351+
352+
# If the filename that has been provided is an iOS/tvOS/watchOS
353+
# .fwork file, dereference the location to the true origin of the
354+
# binary.
355+
if name.endswith(".fwork"):
356+
with open(name) as f:
357+
name = _os.path.join(
358+
_os.path.dirname(_sys.executable),
359+
f.read().strip()
360+
)
361+
351362
self._name = name
352363
flags = self._func_flags_
353364
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

@@ -1714,6 +1714,46 @@ def __repr__(self):
17141714
return f'FileFinder({self.path!r})'
17151715

17161716

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

17191759
def _fix_up_module(ns, name, pathname, cpathname=None):
@@ -1746,10 +1786,17 @@ def _get_supported_file_loaders():
17461786
17471787
Each item is a tuple (loader, suffixes).
17481788
"""
1749-
extensions = ExtensionFileLoader, _imp.extension_suffixes()
1789+
if sys.platform in {"ios", "tvos", "watchos"}:
1790+
extension_loaders = [(AppleFrameworkLoader, [
1791+
suffix.replace(".so", ".fwork")
1792+
for suffix in _imp.extension_suffixes()
1793+
])]
1794+
else:
1795+
extension_loaders = []
1796+
extension_loaders.append((ExtensionFileLoader, _imp.extension_suffixes()))
17501797
source = SourceFileLoader, SOURCE_SUFFIXES
17511798
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
1752-
return [extensions, source, bytecode]
1799+
return extension_loaders + [source, bytecode]
17531800

17541801

17551802
def _set_bootstrap_module(_bootstrap_module):

‎Lib/importlib/abc.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,11 @@ def get_code(self, fullname):
180180
else:
181181
return self.source_to_code(source, path)
182182

183-
_register(ExecutionLoader, machinery.ExtensionFileLoader)
183+
_register(
184+
ExecutionLoader,
185+
machinery.ExtensionFileLoader,
186+
machinery.AppleFrameworkLoader,
187+
)
184188

185189

186190
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

+6-1
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,10 @@ def getsourcefile(object):
954954
elif any(filename.endswith(s) for s in
955955
importlib.machinery.EXTENSION_SUFFIXES):
956956
return None
957+
elif filename.endswith(".fwork"):
958+
# Apple mobile framework markers are another type of non-source file
959+
return None
960+
957961
# return a filename found in the linecache even if it doesn't exist on disk
958962
if filename in linecache.cache:
959963
return filename
@@ -984,6 +988,7 @@ def getmodule(object, _filename=None):
984988
return object
985989
if hasattr(object, '__module__'):
986990
return sys.modules.get(object.__module__)
991+
987992
# Try the filename to modulename cache
988993
if _filename is not None and _filename in modulesbyfile:
989994
return sys.modules.get(modulesbyfile[_filename])
@@ -1119,7 +1124,7 @@ def findsource(object):
11191124
# Allow filenames in form of "<something>" to pass through.
11201125
# `doctest` monkeypatches `linecache` module to enable
11211126
# inspection, so let `linecache.getlines` to be called.
1122-
if not (file.startswith('<') and file.endswith('>')):
1127+
if (not (file.startswith('<') and file.endswith('>'))) or file.endswith('.fwork'):
11231128
raise OSError('source code not available')
11241129

11251130
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
@@ -2056,14 +2056,21 @@ def test_module_state_shared_in_global(self):
20562056
self.addCleanup(os.close, r)
20572057
self.addCleanup(os.close, w)
20582058

2059+
# Apple extensions must be distributed as frameworks. This requires
2060+
# a specialist loader.
2061+
if support.is_apple_mobile:
2062+
loader = "AppleFrameworkLoader"
2063+
else:
2064+
loader = "ExtensionFileLoader"
2065+
20592066
script = textwrap.dedent(f"""
20602067
import importlib.machinery
20612068
import importlib.util
20622069
import os
20632070
20642071
fullname = '_test_module_state_shared'
20652072
origin = importlib.util.find_spec('_testmultiphase').origin
2066-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
2073+
loader = importlib.machinery.{loader}(fullname, origin)
20672074
spec = importlib.util.spec_from_loader(fullname, loader)
20682075
module = importlib.util.module_from_spec(spec)
20692076
attr_id = str(id(module.Error)).encode()
@@ -2371,7 +2378,12 @@ class Test_ModuleStateAccess(unittest.TestCase):
23712378
def setUp(self):
23722379
fullname = '_testmultiphase_meth_state_access' # XXX
23732380
origin = importlib.util.find_spec('_testmultiphase').origin
2374-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
2381+
# Apple extensions must be distributed as frameworks. This requires
2382+
# a specialist loader.
2383+
if support.is_apple_mobile:
2384+
loader = importlib.machinery.AppleFrameworkLoader(fullname, origin)
2385+
else:
2386+
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
23752387
spec = importlib.util.spec_from_loader(fullname, loader)
23762388
module = importlib.util.module_from_spec(spec)
23772389
loader.exec_module(module)

0 commit comments

Comments
 (0)
Please sign in to comment.