Skip to content

Commit

Permalink
Support Python 2.7 framework-style distributions on macOS 12
Browse files Browse the repository at this point in the history
virtualenv's trick of adding a symlink to the base Python dylib
(`/Library/Frameworks/Python.framework/Versions/2.7/Python`) in the
virtualenv root, and rewriting the dependent dylib list of the
`<virtualenv>/bin/python` binary with the path of this symlink, no
longer works in macOS 12.

As of macOS 12, when <https://github.com/python/cpython/blob/v2.7.18/Modules/getpath.c>
computes `sys.prefix`, it ends up resolving the symlink. The result is
that `sys.prefix` points to the Python framework, e.g.
`/path/to/Python.framework/Versions/2.7` instead of at the virtualenv
root.

To mitigate this, add a _copy_ of the Python dylib into the virtualenv
instead of a symlink. To placate code signing (at least on Intel
machines), add a symlink to the `Resources` dir so the `Info.plist` file
is accessible.

Note that Apple's Python 2.7 distribution -- removed in macOS 12.3 but
previously located at /System/Library/Frameworks/Python.framework --
behaved differently to the python.org builds. It appears to have
incorporated custom patches to getpath.c to support virtualenv. See:
<https://github.com/apple-oss-distributions/python/blob/python-170.80.2/2.7/fix/getpath.c.ed>.
  • Loading branch information
nickhutchinson committed Apr 2, 2022
1 parent 1aa302f commit 4fb405a
Show file tree
Hide file tree
Showing 2 changed files with 27 additions and 20 deletions.
1 change: 1 addition & 0 deletions docs/changelog/2284.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support for creating a virtual environment from a Python 2.7 framework on macOS 12 - by :user:`nickhutchinson`.
46 changes: 26 additions & 20 deletions src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,20 +25,11 @@ class CPythonmacOsFramework(CPython):
def can_describe(cls, interpreter):
return is_mac_os_framework(interpreter) and super(CPythonmacOsFramework, cls).can_describe(interpreter)

@classmethod
def sources(cls, interpreter):
for src in super(CPythonmacOsFramework, cls).sources(interpreter):
yield src
# add a symlink to the host python image
exe = cls.image_ref(interpreter)
ref = PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK)
yield ref

def create(self):
super(CPythonmacOsFramework, self).create()

# change the install_name of the copied python executables
target = "@executable_path/../.Python"
target = self.desired_mach_o_image_path()
current = self.current_mach_o_image_path()
for src in self._sources:
if isinstance(src, ExePathRefToDest):
Expand All @@ -62,8 +53,8 @@ def _executables(cls, interpreter):
def current_mach_o_image_path(self):
raise NotImplementedError

@classmethod
def image_ref(cls, interpreter):
@abstractmethod
def desired_mach_o_image_path(self):
raise NotImplementedError


Expand All @@ -74,13 +65,12 @@ def can_create(cls, interpreter):
return super(CPython2macOsFramework, cls).can_create(interpreter)
return False

@classmethod
def image_ref(cls, interpreter):
return Path(interpreter.prefix) / "Python"

def current_mach_o_image_path(self):
return os.path.join(self.interpreter.prefix, "Python")

def desired_mach_o_image_path(self):
return "@executable_path/../Python"

@classmethod
def sources(cls, interpreter):
for src in super(CPython2macOsFramework, cls).sources(interpreter):
Expand All @@ -89,6 +79,14 @@ def sources(cls, interpreter):
exec_marker_file, to_path, _ = cls.from_stdlib(cls.mappings(interpreter), "lib-dynload")
yield PathRefToDest(exec_marker_file, dest=to_path)

# add a copy of the host python image
exe = Path(interpreter.prefix) / "Python"
yield PathRefToDest(exe, dest=lambda self, _: self.dest / "Python", must=RefMust.COPY)

# add a symlink to the Resources dir
resources = Path(interpreter.prefix) / "Resources"
yield PathRefToDest(resources, dest=lambda self, _: self.dest / "Resources")

@property
def reload_code(self):
result = super(CPython2macOsFramework, self).reload_code
Expand Down Expand Up @@ -147,13 +145,21 @@ def fix_signature(self):


class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix):
@classmethod
def image_ref(cls, interpreter):
return Path(interpreter.prefix) / "Python3"

def current_mach_o_image_path(self):
return "@executable_path/../../../../Python3"

def desired_mach_o_image_path(self):
return "@executable_path/../.Python"

@classmethod
def sources(cls, interpreter):
for src in super(CPython3macOsFramework, cls).sources(interpreter):
yield src

# add a symlink to the host python image
exe = Path(interpreter.prefix) / "Python3"
yield PathRefToDest(exe, dest=lambda self, _: self.dest / ".Python", must=RefMust.SYMLINK)

@property
def reload_code(self):
result = super(CPython3macOsFramework, self).reload_code
Expand Down

0 comments on commit 4fb405a

Please sign in to comment.