Skip to content

Commit 3816e7c

Browse files
authored
macOS Python 3 Framework support (#1711)
Signed-off-by: Bernat Gabor <bgabor8@bloomberg.net>
1 parent 06f8cd1 commit 3816e7c

File tree

7 files changed

+98
-34
lines changed

7 files changed

+98
-34
lines changed

setup.cfg

+1
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ virtualenv.create =
6969
cpython3-win = virtualenv.create.via_global_ref.builtin.cpython.cpython3:CPython3Windows
7070
cpython2-posix = virtualenv.create.via_global_ref.builtin.cpython.cpython2:CPython2Posix
7171
cpython2-mac-framework = virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython2macOsFramework
72+
cpython3-mac-framework = virtualenv.create.via_global_ref.builtin.cpython.mac_os:CPython3macOsFramework
7273
cpython2-win = virtualenv.create.via_global_ref.builtin.cpython.cpython2:CPython2Windows
7374
pypy2-posix = virtualenv.create.via_global_ref.builtin.pypy.pypy2:PyPy2Posix
7475
pypy2-win = virtualenv.create.via_global_ref.builtin.pypy.pypy2:Pypy2Windows

src/virtualenv/create/via_global_ref/builtin/cpython/common.py

+8
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,11 @@ def _executables(cls, interpreter):
4646
# for more info on pythonw.exe see https://stackoverflow.com/a/30313091
4747
python_w = host.parent / "pythonw.exe"
4848
yield python_w, [python_w.name]
49+
50+
51+
def is_mac_os_framework(interpreter):
52+
if interpreter.platform == "darwin":
53+
framework_var = interpreter.sysconfig_vars.get("PYTHONFRAMEWORK")
54+
value = "Python3" if interpreter.version_info.major == 3 else "Python"
55+
return framework_var == value
56+
return False

src/virtualenv/create/via_global_ref/builtin/cpython/cpython2.py

+1-6
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from virtualenv.util.path import Path
1010

1111
from ..python2.python2 import Python2
12-
from .common import CPython, CPythonPosix, CPythonWindows
12+
from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework
1313

1414

1515
@add_metaclass(abc.ABCMeta)
@@ -50,11 +50,6 @@ def ensure_directories(self):
5050
return dirs
5151

5252

53-
def is_mac_os_framework(interpreter):
54-
framework = bool(interpreter.sysconfig_vars.get("PYTHONFRAMEWORK"))
55-
return framework and interpreter.platform == "darwin"
56-
57-
5853
class CPython2Posix(CPython2, CPythonPosix):
5954
"""CPython 2 on POSIX (excluding macOs framework builds)"""
6055

src/virtualenv/create/via_global_ref/builtin/cpython/cpython3.py

+4-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
99
from virtualenv.util.path import Path
1010

11-
from .common import CPython, CPythonPosix, CPythonWindows
11+
from .common import CPython, CPythonPosix, CPythonWindows, is_mac_os_framework
1212

1313

1414
@add_metaclass(abc.ABCMeta)
@@ -17,7 +17,9 @@ class CPython3(CPython, Python3Supports):
1717

1818

1919
class CPython3Posix(CPythonPosix, CPython3):
20-
""""""
20+
@classmethod
21+
def can_describe(cls, interpreter):
22+
return is_mac_os_framework(interpreter) is False and super(CPython3Posix, cls).can_describe(interpreter)
2123

2224

2325
class CPython3Windows(CPythonWindows, CPython3):

src/virtualenv/create/via_global_ref/builtin/cpython/mac_os.py

+81-23
Original file line numberDiff line numberDiff line change
@@ -4,59 +4,89 @@
44
import os
55
import struct
66
import subprocess
7+
from abc import ABCMeta, abstractmethod
78
from textwrap import dedent
89

9-
from virtualenv.create.via_global_ref.builtin.cpython.common import CPythonPosix
10-
from virtualenv.create.via_global_ref.builtin.ref import PathRefToDest
10+
from six import add_metaclass
11+
12+
from virtualenv.create.via_global_ref.builtin.ref import ExePathRefToDest, PathRefToDest
1113
from virtualenv.util.path import Path
1214
from virtualenv.util.six import ensure_text
1315

14-
from .cpython2 import CPython2, is_mac_os_framework
16+
from .common import CPython, CPythonPosix, is_mac_os_framework
17+
from .cpython2 import CPython2
18+
from .cpython3 import CPython3
1519

1620

17-
class CPython2macOsFramework(CPython2, CPythonPosix):
21+
@add_metaclass(ABCMeta)
22+
class CPythonmacOsFramework(CPython):
1823
@classmethod
1924
def can_describe(cls, interpreter):
20-
return is_mac_os_framework(interpreter) and super(CPython2macOsFramework, cls).can_describe(interpreter)
21-
22-
def create(self):
23-
super(CPython2macOsFramework, self).create()
24-
25-
# change the install_name of the copied python executable
26-
current = os.path.join(self.interpreter.prefix, "Python")
27-
fix_mach_o(str(self.exe), current, "@executable_path/../.Python", self.interpreter.max_size)
25+
return is_mac_os_framework(interpreter) and super(CPythonmacOsFramework, cls).can_describe(interpreter)
2826

2927
@classmethod
3028
def sources(cls, interpreter):
31-
for src in super(CPython2macOsFramework, cls).sources(interpreter):
29+
for src in super(CPythonmacOsFramework, cls).sources(interpreter):
3230
yield src
33-
34-
# landmark for exec_prefix
35-
name = "lib-dynload"
36-
yield PathRefToDest(interpreter.stdlib_path(name), dest=cls.to_stdlib)
37-
38-
# this must symlink to the host prefix Python
39-
marker = Path(interpreter.prefix) / "Python"
40-
ref = PathRefToDest(marker, dest=lambda self, _: self.dest / ".Python", must_symlink=True)
31+
# add a symlink to the host python image
32+
ref = PathRefToDest(cls.image_ref(interpreter), dest=lambda self, _: self.dest / ".Python", must_symlink=True)
4133
yield ref
4234

35+
def create(self):
36+
super(CPythonmacOsFramework, self).create()
37+
38+
# change the install_name of the copied python executables
39+
target = "@executable_path/../.Python"
40+
current = self.current_mach_o_image_path()
41+
for src in self._sources:
42+
if isinstance(src, ExePathRefToDest):
43+
if src.must_copy or not self.symlinks:
44+
exes = [self.bin_dir / src.base]
45+
if not self.symlinks:
46+
exes.extend(self.bin_dir / a for a in src.aliases)
47+
for exe in exes:
48+
fix_mach_o(str(exe), current, target, self.interpreter.max_size)
49+
4350
@classmethod
4451
def _executables(cls, interpreter):
45-
for _, targets in super(CPython2macOsFramework, cls)._executables(interpreter):
52+
for _, targets in super(CPythonmacOsFramework, cls)._executables(interpreter):
4653
# Make sure we use the embedded interpreter inside the framework, even if sys.executable points to the
4754
# stub executable in ${sys.prefix}/bin.
4855
# See http://groups.google.com/group/python-virtualenv/browse_thread/thread/17cab2f85da75951
4956
fixed_host_exe = Path(interpreter.prefix) / "Resources" / "Python.app" / "Contents" / "MacOS" / "Python"
5057
yield fixed_host_exe, targets
5158

59+
@abstractmethod
60+
def current_mach_o_image_path(self):
61+
raise NotImplementedError
62+
63+
@classmethod
64+
def image_ref(cls, interpreter):
65+
raise NotImplementedError
66+
67+
68+
class CPython2macOsFramework(CPythonmacOsFramework, CPython2, CPythonPosix):
69+
@classmethod
70+
def image_ref(cls, interpreter):
71+
return Path(interpreter.prefix) / "Python"
72+
73+
def current_mach_o_image_path(self):
74+
return os.path.join(self.interpreter.prefix, "Python")
75+
76+
@classmethod
77+
def sources(cls, interpreter):
78+
for src in super(CPython2macOsFramework, cls).sources(interpreter):
79+
yield src
80+
name = "lib-dynload" # landmark for exec_prefix
81+
yield PathRefToDest(interpreter.stdlib_path(name), dest=cls.to_stdlib)
82+
5283
@property
5384
def reload_code(self):
5485
result = super(CPython2macOsFramework, self).reload_code
5586
result = dedent(
5687
"""
5788
# the bundled site.py always adds the global site package if we're on python framework build, escape this
5889
import sysconfig
59-
6090
config = sysconfig.get_config_vars()
6191
before = config["PYTHONFRAMEWORK"]
6292
try:
@@ -71,6 +101,34 @@ def reload_code(self):
71101
return result
72102

73103

104+
class CPython3macOsFramework(CPythonmacOsFramework, CPython3, CPythonPosix):
105+
@classmethod
106+
def image_ref(cls, interpreter):
107+
return Path(interpreter.prefix) / "Python3"
108+
109+
def current_mach_o_image_path(self):
110+
return "@executable_path/../../../../Python3"
111+
112+
@property
113+
def reload_code(self):
114+
result = super(CPython3macOsFramework, self).reload_code
115+
result = dedent(
116+
"""
117+
# the bundled site.py always adds the global site package if we're on python framework build, escape this
118+
import sys
119+
before = sys._framework
120+
try:
121+
sys._framework = None
122+
{}
123+
finally:
124+
sys._framework = before
125+
""".format(
126+
result
127+
)
128+
)
129+
return result
130+
131+
74132
def fix_mach_o(exe, current, new, max_size):
75133
"""
76134
https://en.wikipedia.org/wiki/Mach-O

src/virtualenv/discovery/py_info.py

+2
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,8 @@ def _fast_get_system_executable(self):
112112
def _distutils_install():
113113
# follow https://github.com/pypa/pip/blob/master/src/pip/_internal/locations.py#L95
114114
d = Distribution({"script_args": "--no-user-cfg"}) # configuration files not parsed so they do not hijack paths
115+
if hasattr(sys, "_framework"):
116+
sys._framework = None # disable macOS static paths for framework
115117
i = d.get_command_obj("install", create=True)
116118
i.prefix = os.sep # paths generated are relative to prefix that contains the path sep, this makes it relative
117119
i.finalize_options()

tests/unit/create/test_creator.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -296,7 +296,7 @@ def cross_python(is_inside_ci, session_app_data):
296296

297297

298298
@pytest.mark.slow
299-
def test_cross_major(cross_python, coverage_env, tmp_path, current_fastest, session_app_data):
299+
def test_cross_major(cross_python, coverage_env, tmp_path, session_app_data, current_fastest):
300300
cmd = [
301301
"-v",
302302
"-v",
@@ -307,8 +307,6 @@ def test_cross_major(cross_python, coverage_env, tmp_path, current_fastest, sess
307307
"--no-wheel",
308308
"--activators",
309309
"",
310-
"--creator",
311-
current_fastest,
312310
]
313311
result = cli_run(cmd)
314312
pip_scripts = {i.name.replace(".exe", "") for i in result.creator.script_dir.iterdir() if i.name.startswith("pip")}

0 commit comments

Comments
 (0)