Skip to content

gh-114099 - Add iOS framework loading machinery. #116454

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions Doc/library/importlib.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1241,6 +1241,74 @@ find and load modules.
and how the module's :attr:`__file__` is populated.


.. class:: AppleFrameworkLoader(fullname, dylib_path, parent_paths=None)

A specialization of :class:`importlib.machinery.ExtensionFileLoader` that
is able to load extension modules in Framework format.

For compatibility with the iOS App Store, *all* binary modules in an iOS app
must be ``.dylib objects``, contained in a framework with appropriate
metadata, stored in the ``Frameworks`` folder of the packaged app. There can
be only a single binary per framework, and there can be no executable binary
material outside the Frameworks folder.

If you're trying to run ``from foo.bar import _whiz``, and ``_whiz`` is
implemented with the binary module ``foo/bar/_whiz.abi3.dylib`` (or any
other ABI .dylib extension), this loader will look for
``{sys.executable}/Frameworks/foo.bar._whiz.framework/_whiz.abi3.dylib``
(forming the package name by taking the full import path of the library,
and replacing ``/`` with ``.``).

However, this loader will re-write the ``__file__`` attribute of the
``_whiz`` module will report as the original location inside the ``foo/bar``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
``_whiz`` module will report as the original location inside the ``foo/bar``
``_whiz`` module to report the original location inside the ``foo/bar``

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've reworked this sentence, so the suggestion no longer applies.

subdirectory. This so that code that depends on walking directory trees will
continue to work as expected based on the *original* file location.

The *fullname* argument specifies the name of the module the loader is to
support. The *dylib_path* argument is the path to the framework's ``.dylib``
file. The ``parent_paths`` is the path or paths that was searched to find
the extension module.

.. versionadded:: 3.13

.. availability:: iOS.

.. attribute:: fullname

Name of the module the loader supports.

.. attribute:: dylib_path

Path to the ``.dylib`` file in the framework.

.. attribute:: parent_paths

The parent paths that were originally searched to find the module.

.. class:: AppleFrameworkFinder(framework_path)

An extension module finder which is able to load extension modules packaged
as frameworks in an iOS app.

See the documentation for :class:`AppleFrameworkLoader` for details on the
requirements of binary extension modules on iOS.

The *framework_path* argument is the Frameworks directory for the app.

.. versionadded:: 3.13

.. availability:: iOS.

.. attribute:: framework_path

The path the finder will search for frameworks.

.. method:: find_spec(fullname, paths, target=None)

Attempt to find the spec to handle ``fullname``, imported from one
of the filesystem locations described by ``paths``.


:mod:`importlib.util` -- Utility code for importers
---------------------------------------------------

Expand Down
2 changes: 1 addition & 1 deletion Doc/tools/extensions/pyspecific.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ class Availability(SphinxDirective):
known_platforms = frozenset({
"AIX", "Android", "BSD", "DragonFlyBSD", "Emscripten", "FreeBSD",
"GNU/kFreeBSD", "Linux", "NetBSD", "OpenBSD", "POSIX", "Solaris",
"Unix", "VxWorks", "WASI", "Windows", "macOS",
"Unix", "VxWorks", "WASI", "Windows", "macOS", "iOS",
# libc
"BSD libc", "glibc", "musl",
# POSIX platforms with pthreads
Expand Down
41 changes: 21 additions & 20 deletions Lib/importlib/_bootstrap_external.py
Original file line number Diff line number Diff line change
Expand Up @@ -1713,12 +1713,13 @@ def __repr__(self):

class AppleFrameworkLoader(ExtensionFileLoader):
"""A loader for modules that have been packaged as frameworks for
compatibility with Apple's App Store policies.
compatibility with Apple's iOS App Store policies.

For compatibility with the App Store, *all* binary modules must be .dylib
objects, contained in a framework, stored in the ``Frameworks`` folder of
the packaged app. There can be only a single binary per framework, and
there can be no executable binary material outside the Frameworks folder.
For compatibility with the iOS App Store, *all* binary modules in an iOS
app must be .dylib objects, contained in a framework with appropriate
metadata, stored in the ``Frameworks`` folder of the packaged app. There
can be only a single binary per framework, and there can be no executable
binary material outside the Frameworks folder.

If you're trying to run ``from foo.bar import _whiz``, and ``_whiz`` is
implemented with the binary module ``foo/bar/_whiz.abi3.dylib`` (or any
Expand All @@ -1739,9 +1740,9 @@ class AppleFrameworkLoader(ExtensionFileLoader):
be done with a build step in the Xcode project; see the iOS documentation
for details on how to construct this build step.
"""
def __init__(self, fullname, dylib_file, path=None):
def __init__(self, fullname, dylib_file, parent_paths=None):
super().__init__(fullname, dylib_file)
self.parent_paths = path
self.parent_paths = parent_paths

def create_module(self, spec):
mod = super().create_module(spec)
Expand All @@ -1752,7 +1753,7 @@ def create_module(self, spec):
parent_path,
_path_split(self.path)[-1],
)
continue
break
return mod


Expand All @@ -1762,23 +1763,23 @@ class AppleFrameworkFinder:

See AppleFrameworkLoader for details.
"""
def __init__(self, path):
self.frameworks_path = path
def __init__(self, frameworks_path):
self.frameworks_path = frameworks_path

def find_spec(self, fullname, path, target=None):
def find_spec(self, fullname, paths, target=None):
name = fullname.split(".")[-1]

for extension in EXTENSION_SUFFIXES:
dylib_file = _path_join(self.frameworks_path, f"{fullname}.framework", f"{name}{extension}")
dylib_file = _path_join(
self.frameworks_path,
f"{fullname}.framework", f"{name}{extension}"
)
_bootstrap._verbose_message("Looking for Apple Framework dylib {}", dylib_file)
try:
dylib_exists = _path_isfile(dylib_file)
except ValueError:
pass
else:
if dylib_exists:
loader = AppleFrameworkLoader(fullname, dylib_file, path)
return _bootstrap.spec_from_loader(fullname, loader)

dylib_exists = _path_isfile(dylib_file)
if dylib_exists:
loader = AppleFrameworkLoader(fullname, dylib_file, paths)
return _bootstrap.spec_from_loader(fullname, loader)

return None

Expand Down
10 changes: 3 additions & 7 deletions Python/dynload_shlib.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,9 @@ const char *_PyImport_DynLoadFiletab[] = {
#ifdef __CYGWIN__
".dll",
#else /* !__CYGWIN__ */
# ifdef __APPLE__
// TARGET_OS_IPHONE covers any non-macOS Apple platform.
# if TARGET_OS_IPHONE
# define SHLIB_SUFFIX ".dylib"
# else
# define SHLIB_SUFFIX ".so"
# endif
// TARGET_OS_IPHONE covers any non-macOS Apple platform.
# if defined(__APPLE__) && TARGET_OS_IPHONE
# define SHLIB_SUFFIX ".dylib"
# else
# define SHLIB_SUFFIX ".so"
# endif
Expand Down