Skip to content

Commit 4617ca0

Browse files
freakboy3742mhsmithericsnowcurrently
committed
[3.9] 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 fd5a4aa commit 4617ca0

File tree

20 files changed

+3008
-2750
lines changed

20 files changed

+3008
-2750
lines changed

.gitignore

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ Lib/test/data/*
6161
!Lib/test/data/README
6262
/Makefile
6363
/Makefile.pre
64-
iOSTestbed.*
64+
/iOSTestbed.*
6565
iOS/Frameworks/
6666
iOS/Resources/Info.plist
6767
iOS/testbed/build

Doc/library/importlib.rst

+63
Original file line numberDiff line numberDiff line change
@@ -1448,6 +1448,69 @@ find and load modules.
14481448
Boolean indicating whether or not the module's "origin"
14491449
attribute refers to a loadable location.
14501450

1451+
.. class:: AppleFrameworkLoader(name, path)
1452+
1453+
A specialization of :class:`importlib.machinery.ExtensionFileLoader` that
1454+
is able to load extension modules in Framework format.
1455+
1456+
For compatibility with the iOS App Store, *all* binary modules in an iOS app
1457+
must be dynamic libraries, contained in a framework with appropriate
1458+
metadata, stored in the ``Frameworks`` folder of the packaged app. There can
1459+
be only a single binary per framework, and there can be no executable binary
1460+
material outside the Frameworks folder.
1461+
1462+
To accomodate this requirement, when running on iOS, extension module
1463+
binaries are *not* packaged as ``.so`` files on ``sys.path``, but as
1464+
individual standalone frameworks. To discover those frameworks, this loader
1465+
is be registered against the ``.fwork`` file extension, with a ``.fwork``
1466+
file acting as a placeholder in the original location of the binary on
1467+
``sys.path``. The ``.fwork`` file contains the path of the actual binary in
1468+
the ``Frameworks`` folder, relative to the app bundle. To allow for
1469+
resolving a framework-packaged binary back to the original location, the
1470+
framework is expected to contain a ``.origin`` file that contains the
1471+
location of the ``.fwork`` file, relative to the app bundle.
1472+
1473+
For example, consider the case of an import ``from foo.bar import _whiz``,
1474+
where ``_whiz`` is implemented with the binary module
1475+
``sources/foo/bar/_whiz.abi3.so``, with ``sources`` being the location
1476+
registered on ``sys.path``, relative to the application bundle. This module
1477+
*must* be distributed as
1478+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz`` (creating the framework
1479+
name from the full import path of the module), with an ``Info.plist`` file
1480+
in the ``.framework`` directory identifying the binary as a framework. The
1481+
``foo.bar._whiz`` module would be represented in the original location with
1482+
a ``sources/foo/bar/_whiz.abi3.fwork`` marker file, containing the path
1483+
``Frameworks/foo.bar._whiz/foo.bar._whiz``. The framework would also contain
1484+
``Frameworks/foo.bar._whiz.framework/foo.bar._whiz.origin``, containing the
1485+
path to the ``.fwork`` file.
1486+
1487+
When a module is loaded with this loader, the ``__file__`` for the module
1488+
will report as the location of the ``.fwork`` file. This allows code to use
1489+
the ``__file__`` of a module as an anchor for file system traveral.
1490+
However, the spec origin will reference the location of the *actual* binary
1491+
in the ``.framework`` folder.
1492+
1493+
The Xcode project building the app is responsible for converting any ``.so``
1494+
files from wherever they exist in the ``PYTHONPATH`` into frameworks in the
1495+
``Frameworks`` folder (including stripping extensions from the module file,
1496+
the addition of framework metadata, and signing the resulting framework),
1497+
and creating the ``.fwork`` and ``.origin`` files. This will usually be done
1498+
with a build step in the Xcode project; see the iOS documentation for
1499+
details on how to construct this build step.
1500+
1501+
.. versionadded:: 3.13
1502+
1503+
.. availability:: iOS.
1504+
1505+
.. attribute:: name
1506+
1507+
Name of the module the loader supports.
1508+
1509+
.. attribute:: path
1510+
1511+
Path to the ``.fwork`` file for the extension module.
1512+
1513+
14511514
:mod:`importlib.util` -- Utility code for importers
14521515
---------------------------------------------------
14531516

Lib/ctypes/__init__.py

+13
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,19 @@ def __init__(self, name, mode=DEFAULT_MODE, handle=None,
341341
use_errno=False,
342342
use_last_error=False,
343343
winmode=None):
344+
if name:
345+
name = _os.fspath(name)
346+
347+
# If the filename that has been provided is an iOS/tvOS/watchOS
348+
# .fwork file, dereference the location to the true origin of the
349+
# binary.
350+
if name.endswith(".fwork"):
351+
with open(name) as f:
352+
name = _os.path.join(
353+
_os.path.dirname(_sys.executable),
354+
f.read().strip()
355+
)
356+
344357
self._name = name
345358
flags = self._func_flags_
346359
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
@@ -49,7 +49,7 @@
4949

5050
# Bootstrap-related code ######################################################
5151
_CASE_INSENSITIVE_PLATFORMS_STR_KEY = 'win',
52-
_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin'
52+
_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY = 'cygwin', 'darwin', 'ios', 'tvos', 'watchos'
5353
_CASE_INSENSITIVE_PLATFORMS = (_CASE_INSENSITIVE_PLATFORMS_BYTES_KEY
5454
+ _CASE_INSENSITIVE_PLATFORMS_STR_KEY)
5555

@@ -1615,6 +1615,46 @@ def __repr__(self):
16151615
return 'FileFinder({!r})'.format(self.path)
16161616

16171617

1618+
class AppleFrameworkLoader(ExtensionFileLoader):
1619+
"""A loader for modules that have been packaged as frameworks for
1620+
compatibility with Apple's iOS App Store policies.
1621+
"""
1622+
def create_module(self, spec):
1623+
# If the ModuleSpec has been created by the FileFinder, it will have
1624+
# been created with an origin pointing to the .fwork file. We need to
1625+
# redirect this to the location in the Frameworks folder, using the
1626+
# content of the .fwork file.
1627+
if spec.origin.endswith(".fwork"):
1628+
with _io.FileIO(spec.origin, 'r') as file:
1629+
framework_binary = file.read().decode().strip()
1630+
bundle_path = _path_split(sys.executable)[0]
1631+
spec.origin = _path_join(bundle_path, framework_binary)
1632+
1633+
# If the loader is created based on the spec for a loaded module, the
1634+
# path will be pointing at the Framework location. If this occurs,
1635+
# get the original .fwork location to use as the module's __file__.
1636+
if self.path.endswith(".fwork"):
1637+
path = self.path
1638+
else:
1639+
with _io.FileIO(self.path + ".origin", 'r') as file:
1640+
origin = file.read().decode().strip()
1641+
bundle_path = _path_split(sys.executable)[0]
1642+
path = _path_join(bundle_path, origin)
1643+
1644+
module = _bootstrap._call_with_frames_removed(_imp.create_dynamic, spec)
1645+
1646+
_bootstrap._verbose_message(
1647+
"Apple framework extension module {!r} loaded from {!r} (path {!r})",
1648+
spec.name,
1649+
spec.origin,
1650+
path,
1651+
)
1652+
1653+
# Ensure that the __file__ points at the .fwork location
1654+
module.__file__ = path
1655+
1656+
return module
1657+
16181658
# Import setup ###############################################################
16191659

16201660
def _fix_up_module(ns, name, pathname, cpathname=None):
@@ -1645,10 +1685,17 @@ def _get_supported_file_loaders():
16451685
16461686
Each item is a tuple (loader, suffixes).
16471687
"""
1648-
extensions = ExtensionFileLoader, _imp.extension_suffixes()
1688+
if sys.platform in {"ios", "tvos", "watchos"}:
1689+
extension_loaders = [(AppleFrameworkLoader, [
1690+
suffix.replace(".so", ".fwork")
1691+
for suffix in _imp.extension_suffixes()
1692+
])]
1693+
else:
1694+
extension_loaders = []
1695+
extension_loaders.append((ExtensionFileLoader, _imp.extension_suffixes()))
16491696
source = SourceFileLoader, SOURCE_SUFFIXES
16501697
bytecode = SourcelessFileLoader, BYTECODE_SUFFIXES
1651-
return [extensions, source, bytecode]
1698+
return extension_loaders + [source, bytecode]
16521699

16531700

16541701
def _setup(_bootstrap_module):

Lib/importlib/abc.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,11 @@ def get_code(self, fullname):
284284
else:
285285
return self.source_to_code(source, path)
286286

287-
_register(ExecutionLoader, machinery.ExtensionFileLoader)
287+
_register(
288+
ExecutionLoader,
289+
machinery.ExtensionFileLoader,
290+
machinery.AppleFrameworkLoader,
291+
)
288292

289293

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

Lib/importlib/machinery.py

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from ._bootstrap_external import SourceFileLoader
1515
from ._bootstrap_external import SourcelessFileLoader
1616
from ._bootstrap_external import ExtensionFileLoader
17+
from ._bootstrap_external import AppleFrameworkLoader
1718

1819

1920
def all_suffixes():

Lib/inspect.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,7 @@ def getmodule(object, _filename=None):
730730
return object
731731
if hasattr(object, '__module__'):
732732
return sys.modules.get(object.__module__)
733+
733734
# Try the filename to modulename cache
734735
if _filename is not None and _filename in modulesbyfile:
735736
return sys.modules.get(modulesbyfile[_filename])
@@ -823,7 +824,7 @@ def findsource(object):
823824
# Allow filenames in form of "<something>" to pass through.
824825
# `doctest` monkeypatches `linecache` module to enable
825826
# inspection, so let `linecache.getlines` to be called.
826-
if not (file.startswith('<') and file.endswith('>')):
827+
if (not (file.startswith('<') and file.endswith('>'))) or file.endswith('.fwork'):
827828
raise OSError('source code not available')
828829

829830
module = getmodule(object, file)

Lib/modulefinder.py

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

83-
elif isinstance(spec.loader, importlib.machinery.ExtensionFileLoader):
83+
elif isinstance(
84+
spec.loader, (
85+
importlib.machinery.ExtensionFileLoader,
86+
importlib.machinery.AppleFrameworkLoader,
87+
)
88+
):
8489
kind = _C_EXTENSION
8590

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

Lib/test/test_capi.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -697,14 +697,21 @@ def test_module_state_shared_in_global(self):
697697
self.addCleanup(os.close, r)
698698
self.addCleanup(os.close, w)
699699

700+
# Apple extensions must be distributed as frameworks. This requires
701+
# a specialist loader.
702+
if support.is_apple_mobile:
703+
loader = "AppleFrameworkLoader"
704+
else:
705+
loader = "ExtensionFileLoader"
706+
700707
script = textwrap.dedent(f"""
701708
import importlib.machinery
702709
import importlib.util
703710
import os
704711
705712
fullname = '_test_module_state_shared'
706713
origin = importlib.util.find_spec('_testmultiphase').origin
707-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
714+
loader = importlib.machinery.{loader}(fullname, origin)
708715
spec = importlib.util.spec_from_loader(fullname, loader)
709716
module = importlib.util.module_from_spec(spec)
710717
attr_id = str(id(module.Error)).encode()
@@ -883,7 +890,12 @@ class Test_ModuleStateAccess(unittest.TestCase):
883890
def setUp(self):
884891
fullname = '_testmultiphase_meth_state_access' # XXX
885892
origin = importlib.util.find_spec('_testmultiphase').origin
886-
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
893+
# Apple extensions must be distributed as frameworks. This requires
894+
# a specialist loader.
895+
if support.is_apple_mobile:
896+
loader = importlib.machinery.AppleFrameworkLoader(fullname, origin)
897+
else:
898+
loader = importlib.machinery.ExtensionFileLoader(fullname, origin)
887899
spec = importlib.util.spec_from_loader(fullname, loader)
888900
module = importlib.util.module_from_spec(spec)
889901
loader.exec_module(module)

Lib/test/test_importlib/extension/test_finder.py

+23-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
from .. import abc
2-
from .. import util
1+
from test.support import is_apple_mobile
2+
from test.test_importlib import abc, util
33

44
machinery = util.import_importlib('importlib.machinery')
55

@@ -12,9 +12,27 @@ class FinderTests(abc.FinderTests):
1212
"""Test the finder for extension modules."""
1313

1414
def find_module(self, fullname):
15-
importer = self.machinery.FileFinder(util.EXTENSIONS.path,
16-
(self.machinery.ExtensionFileLoader,
17-
self.machinery.EXTENSION_SUFFIXES))
15+
if is_apple_mobile:
16+
# Apple mobile platforms require a specialist loader that uses
17+
# .fwork files as placeholders for the true `.so` files.
18+
loaders = [
19+
(
20+
self.machinery.AppleFrameworkLoader,
21+
[
22+
ext.replace(".so", ".fwork")
23+
for ext in self.machinery.EXTENSION_SUFFIXES
24+
]
25+
)
26+
]
27+
else:
28+
loaders = [
29+
(
30+
self.machinery.ExtensionFileLoader,
31+
self.machinery.EXTENSION_SUFFIXES
32+
)
33+
]
34+
35+
importer = self.machinery.FileFinder(util.EXTENSIONS.path, *loaders)
1836
with warnings.catch_warnings():
1937
warnings.simplefilter('ignore', DeprecationWarning)
2038
return importer.find_module(fullname)

0 commit comments

Comments
 (0)