Skip to content

Commit c82aa70

Browse files
authored
Modify handling of platform site to allow for venv usage. (#246)
Moves the platform site handling into the framework, and adds a mechanism to convert a macOS virtual environment into a cross-platform iOS build environment.
1 parent a2c04c3 commit c82aa70

9 files changed

+383
-345
lines changed

Makefile

+41-19
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,10 @@ ARCH-$(target)=$$(subst .,,$$(suffix $(target)))
129129
ifneq ($(os),macOS)
130130
ifeq ($$(findstring simulator,$$(SDK-$(target))),)
131131
TARGET_TRIPLE-$(target)=$$(ARCH-$(target))-apple-$$(OS_LOWER-$(target))$$(VERSION_MIN-$(os))
132-
IS_SIMULATOR-$(target)="False"
132+
IS_SIMULATOR-$(target)=False
133133
else
134134
TARGET_TRIPLE-$(target)=$$(ARCH-$(target))-apple-$$(OS_LOWER-$(target))$$(VERSION_MIN-$(os))-simulator
135-
IS_SIMULATOR-$(target)="True"
135+
IS_SIMULATOR-$(target)=True
136136
endif
137137
endif
138138

@@ -261,6 +261,9 @@ PYTHON_LIB-$(target)=$$(PYTHON_FRAMEWORK-$(target))/Python
261261
PYTHON_BIN-$(target)=$$(PYTHON_INSTALL-$(target))/bin
262262
PYTHON_INCLUDE-$(target)=$$(PYTHON_FRAMEWORK-$(target))/Headers
263263
PYTHON_STDLIB-$(target)=$$(PYTHON_INSTALL-$(target))/lib/python$(PYTHON_VER)
264+
PYTHON_PLATFORM_CONFIG-$(target)=$$(PYTHON_INSTALL-$(target))/platform-config/$$(ARCH-$(target))-$$(SDK-$(target))
265+
PYTHON_PLATFORM_SITECUSTOMIZE-$(target)=$$(PYTHON_PLATFORM_CONFIG-$(target))/sitecustomize.py
266+
264267

265268
$$(PYTHON_SRCDIR-$(target))/configure: \
266269
downloads/Python-$(PYTHON_VERSION).tar.gz \
@@ -319,23 +322,35 @@ $$(PYTHON_LIB-$(target)): $$(PYTHON_SRCDIR-$(target))/python.exe
319322
# Remove any .orig files produced by the compliance patching process
320323
find $$(PYTHON_INSTALL-$(target)) -name "*.orig" -exec rm {} \;
321324

322-
endif
323-
324-
PYTHON_SITECUSTOMIZE-$(target)=$(PROJECT_DIR)/support/$(PYTHON_VER)/$(os)/platform-site/$(target)/sitecustomize.py
325325

326-
$$(PYTHON_SITECUSTOMIZE-$(target)):
327-
@echo ">>> Create cross-platform sitecustomize.py for $(target)"
328-
mkdir -p $$(dir $$(PYTHON_SITECUSTOMIZE-$(target)))
329-
cat $(PROJECT_DIR)/patch/Python/sitecustomize.$(os).py \
326+
$$(PYTHON_PLATFORM_SITECUSTOMIZE-$(target)):
327+
@echo ">>> Create cross-plaform config for $(target)"
328+
mkdir -p $$(PYTHON_PLATFORM_CONFIG-$(target))
329+
# Create the cross-platform site definition
330+
echo "import _cross_$$(ARCH-$(target))_$$(SDK-$(target)); import _cross_venv;" \
331+
> $$(PYTHON_PLATFORM_CONFIG-$(target))/_cross_venv.pth
332+
cp $(PROJECT_DIR)/patch/Python/make_cross_venv.py \
333+
$$(PYTHON_PLATFORM_CONFIG-$(target))/make_cross_venv.py
334+
cp $(PROJECT_DIR)/patch/Python/_cross_venv.py \
335+
$$(PYTHON_PLATFORM_CONFIG-$(target))/_cross_venv.py
336+
cp $$(PYTHON_STDLIB-$(target))/_sysconfig* \
337+
$$(PYTHON_PLATFORM_CONFIG-$(target))
338+
cat $(PROJECT_DIR)/patch/Python/_cross_target.py.tmpl \
330339
| sed -e "s/{{os}}/$(os)/g" \
340+
| sed -e "s/{{platform}}/$$(OS_LOWER-$(target))/g" \
331341
| sed -e "s/{{arch}}/$$(ARCH-$(target))/g" \
342+
| sed -e "s/{{sdk}}/$$(SDK-$(target))/g" \
332343
| sed -e "s/{{version_min}}/$$(VERSION_MIN-$(os))/g" \
333344
| sed -e "s/{{is_simulator}}/$$(IS_SIMULATOR-$(target))/g" \
334-
| sed -e "s/{{multiarch}}/$$(ARCH-$(target))-$$(SDK-$(target))/g" \
335-
| sed -e "s/{{tag}}/$$(OS_LOWER-$(target))-$$(VERSION_MIN-$(os))-$$(ARCH-$(target))-$$(SDK-$(target))/g" \
336-
> $$(PYTHON_SITECUSTOMIZE-$(target))
345+
> $$(PYTHON_PLATFORM_CONFIG-$(target))/_cross_$$(ARCH-$(target))_$$(SDK-$(target)).py
346+
cat $(PROJECT_DIR)/patch/Python/sitecustomize.py.tmpl \
347+
| sed -e "s/{{arch}}/$$(ARCH-$(target))/g" \
348+
| sed -e "s/{{sdk}}/$$(SDK-$(target))/g" \
349+
> $$(PYTHON_PLATFORM_SITECUSTOMIZE-$(target))
337350

338-
$(target): $$(PYTHON_SITECUSTOMIZE-$(target)) $$(PYTHON_LIB-$(target))
351+
endif
352+
353+
$(target): $$(PYTHON_PLATFORM_SITECUSTOMIZE-$(target)) $$(PYTHON_LIB-$(target))
339354

340355
###########################################################################
341356
# Target: Debug
@@ -364,6 +379,8 @@ vars-$(target):
364379
@echo "PYTHON_BIN-$(target): $$(PYTHON_BIN-$(target))"
365380
@echo "PYTHON_INCLUDE-$(target): $$(PYTHON_INCLUDE-$(target))"
366381
@echo "PYTHON_STDLIB-$(target): $$(PYTHON_STDLIB-$(target))"
382+
@echo "PYTHON_PLATFORM_CONFIG-$(target): $$(PYTHON_PLATFORM_CONFIG-$(target))"
383+
@echo "PYTHON_PLATFORM_SITECUSTOMIZE-$(target): $$(PYTHON_PLATFORM_SITECUSTOMIZE-$(target))"
367384
@echo
368385

369386
endef # build-target
@@ -424,6 +441,7 @@ PYTHON_LIB-$(sdk)=$$(PYTHON_FRAMEWORK-$(sdk))/Python
424441
PYTHON_BIN-$(sdk)=$$(PYTHON_INSTALL-$(sdk))/bin
425442
PYTHON_INCLUDE-$(sdk)=$$(PYTHON_FRAMEWORK-$(sdk))/Headers
426443
PYTHON_STDLIB-$(sdk)=$$(PYTHON_INSTALL-$(sdk))/lib/python$(PYTHON_VER)
444+
PYTHON_PLATFORM_CONFIG-$(sdk)=$$(PYTHON_INSTALL-$(sdk))/platform-config
427445

428446
$$(PYTHON_LIB-$(sdk)): $$(foreach target,$$(SDK_TARGETS-$(sdk)),$$(PYTHON_LIB-$$(target)))
429447
@echo ">>> Build Python fat library for the $(sdk) SDK"
@@ -459,7 +477,7 @@ $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h: $$(PYTHON_LIB-$(sdk))
459477
cp $$(PYTHON_SRCDIR-$$(firstword $$(SDK_TARGETS-$(sdk))))/$(os)/Resources/pyconfig.h $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h
460478

461479

462-
$$(PYTHON_STDLIB-$(sdk))/LICENSE.TXT: $$(PYTHON_LIB-$(sdk)) $$(PYTHON_FRAMEWORK-$(sdk))/Info.plist $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h
480+
$$(PYTHON_STDLIB-$(sdk))/LICENSE.TXT: $$(PYTHON_LIB-$(sdk)) $$(PYTHON_FRAMEWORK-$(sdk))/Info.plist $$(PYTHON_INCLUDE-$(sdk))/pyconfig.h $$(foreach target,$$(SDK_TARGETS-$(sdk)),$$(PYTHON_PLATFORM_SITECUSTOMIZE-$$(target)))
463481
@echo ">>> Build Python stdlib for the $(sdk) SDK"
464482
mkdir -p $$(PYTHON_STDLIB-$(sdk))/lib-dynload
465483
# Copy stdlib from the first target associated with the $(sdk) SDK
@@ -468,11 +486,17 @@ $$(PYTHON_STDLIB-$(sdk))/LICENSE.TXT: $$(PYTHON_LIB-$(sdk)) $$(PYTHON_FRAMEWORK-
468486
# Delete the single-SDK parts of the standard library
469487
rm -rf \
470488
$$(PYTHON_STDLIB-$(sdk))/_sysconfigdata__*.py \
489+
$$(PYTHON_STDLIB-$(sdk))/_sysconfig_vars__*.json \
471490
$$(PYTHON_STDLIB-$(sdk))/config-* \
472491
$$(PYTHON_STDLIB-$(sdk))/lib-dynload/*
473492

474493
# Copy the individual _sysconfigdata modules into names that include the architecture
475494
$$(foreach target,$$(SDK_TARGETS-$(sdk)),cp $$(PYTHON_STDLIB-$$(target))/_sysconfigdata_* $$(PYTHON_STDLIB-$(sdk))/; )
495+
$$(foreach target,$$(SDK_TARGETS-$(sdk)),cp $$(PYTHON_STDLIB-$$(target))/_sysconfig_vars_* $$(PYTHON_STDLIB-$(sdk))/; )
496+
497+
# Copy the platform site folders for each architecture
498+
mkdir -p $$(PYTHON_PLATFORM_CONFIG-$(sdk))
499+
$$(foreach target,$$(SDK_TARGETS-$(sdk)),cp -r $$(PYTHON_PLATFORM_CONFIG-$$(target)) $$(PYTHON_PLATFORM_CONFIG-$(sdk)); )
476500

477501
# Merge the binary modules from each target in the $(sdk) SDK into a single binary
478502
$$(foreach module,$$(wildcard $$(PYTHON_STDLIB-$$(firstword $$(SDK_TARGETS-$(sdk))))/lib-dynload/*),lipo -create -output $$(PYTHON_STDLIB-$(sdk))/lib-dynload/$$(notdir $$(module)) $$(foreach target,$$(SDK_TARGETS-$(sdk)),$$(PYTHON_STDLIB-$$(target))/lib-dynload/$$(notdir $$(module))); )
@@ -581,7 +605,7 @@ support/$(PYTHON_VER)/macOS/VERSIONS:
581605
dist/Python-$(PYTHON_VER)-macOS-support.$(BUILD_NUMBER).tar.gz: \
582606
$$(PYTHON_XCFRAMEWORK-macOS)/Info.plist \
583607
support/$(PYTHON_VER)/macOS/VERSIONS \
584-
$$(foreach target,$$(TARGETS-macOS), $$(PYTHON_SITECUSTOMIZE-$$(target)))
608+
$$(foreach target,$$(TARGETS-macOS), $$(PYTHON_PLATFORM_SITECUSTOMIZE-$$(target)))
585609

586610
@echo ">>> Create final distribution artefact for macOS"
587611
mkdir -p dist
@@ -604,9 +628,7 @@ $$(PYTHON_XCFRAMEWORK-$(os))/Info.plist: \
604628
$$(foreach sdk,$$(SDKS-$(os)),cp -r $$(PYTHON_INSTALL-$$(sdk))/include $$(PYTHON_XCFRAMEWORK-$(os))/$$(SDK_SLICE-$$(sdk)); )
605629
$$(foreach sdk,$$(SDKS-$(os)),cp -r $$(PYTHON_INSTALL-$$(sdk))/bin $$(PYTHON_XCFRAMEWORK-$(os))/$$(SDK_SLICE-$$(sdk)); )
606630
$$(foreach sdk,$$(SDKS-$(os)),cp -r $$(PYTHON_INSTALL-$$(sdk))/lib $$(PYTHON_XCFRAMEWORK-$(os))/$$(SDK_SLICE-$$(sdk)); )
607-
608-
@echo ">>> Create helper links in XCframework for $(os)"
609-
$$(foreach sdk,$$(SDKS-$(os)),ln -si $$(SDK_SLICE-$$(sdk)) $$(PYTHON_XCFRAMEWORK-$(os))/$$(sdk); )
631+
$$(foreach sdk,$$(SDKS-$(os)),cp -r $$(PYTHON_INSTALL-$$(sdk))/platform-config $$(PYTHON_XCFRAMEWORK-$(os))/$$(SDK_SLICE-$$(sdk)); )
610632

611633
ifeq ($(os),iOS)
612634
@echo ">>> Clone testbed project for $(os)"
@@ -626,7 +648,7 @@ endif
626648

627649
dist/Python-$(PYTHON_VER)-$(os)-support.$(BUILD_NUMBER).tar.gz: \
628650
$$(PYTHON_XCFRAMEWORK-$(os))/Info.plist \
629-
$$(foreach target,$$(TARGETS-$(os)), $$(PYTHON_SITECUSTOMIZE-$$(target)))
651+
$$(foreach target,$$(TARGETS-$(os)), $$(PYTHON_PLATFORM_SITECUSTOMIZE-$$(target)))
630652

631653
@echo ">>> Create final distribution artefact for $(os)"
632654
mkdir -p dist

patch/Python/_cross_target.py.tmpl

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# A site package that turns a macOS virtual environment
2+
# into an {{arch}} {{sdk}} cross-platform virtual environment
3+
import platform
4+
import subprocess
5+
import sys
6+
import sysconfig
7+
8+
###########################################################################
9+
# sys module patches
10+
###########################################################################
11+
sys.cross_compiling = True
12+
sys.platform = "{{platform}}"
13+
sys.implementation._multiarch = "{{arch}}-{{sdk}}"
14+
15+
###########################################################################
16+
# subprocess module patches
17+
###########################################################################
18+
subprocess._can_fork_exec = True
19+
20+
21+
###########################################################################
22+
# platform module patches
23+
###########################################################################
24+
25+
def cross_system():
26+
return "{{os}}"
27+
28+
29+
def cross_uname():
30+
return platform.uname_result(
31+
system="{{os}}",
32+
node="build",
33+
release="{{version_min}}",
34+
version="",
35+
machine="{{arch}}",
36+
)
37+
38+
39+
def cross_ios_ver(system="", release="", model="", is_simulator=False):
40+
if system == "":
41+
system = "{{os}}"
42+
if release == "":
43+
release = "{{version_min}}"
44+
if model == "":
45+
model = "{{sdk}}"
46+
47+
return platform.IOSVersionInfo(system, release, model, {{is_simulator}})
48+
49+
50+
platform.system = cross_system
51+
platform.uname = cross_uname
52+
platform.ios_ver = cross_ios_ver
53+
54+
55+
###########################################################################
56+
# sysconfig module patches
57+
###########################################################################
58+
59+
def cross_get_platform():
60+
return "{{platform}}-{{version_min}}-{{arch}}-{{sdk}}"
61+
62+
63+
def cross_get_sysconfigdata_name():
64+
return "_sysconfigdata__{{platform}}_{{arch}}-{{sdk}}"
65+
66+
67+
sysconfig.get_platform = cross_get_platform
68+
sysconfig._get_sysconfigdata_name = cross_get_sysconfigdata_name
69+
70+
# Force sysconfig data to be loaded (and cached).
71+
sysconfig._init_config_vars()

patch/Python/_cross_venv.py

+105
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import shutil
2+
import sys
3+
import sysconfig
4+
from pathlib import Path
5+
6+
SITE_PACKAGE_PATH = Path(__file__).parent
7+
8+
###########################################################################
9+
# importlib module patches
10+
###########################################################################
11+
12+
13+
def patch_env_create(env):
14+
"""
15+
Patch the process of creating virtual environments to ensure that the cross
16+
environment modification files are also copied as part of environment
17+
creation.
18+
"""
19+
old_pip_env_create = env._PipBackend.create
20+
21+
def pip_env_create(self, path, *args, **kwargs):
22+
result = old_pip_env_create(self, path, *args, **kwargs)
23+
# Copy any _cross_*.pth or _cross_*.py file, plus the cross-platform
24+
# sysconfigdata module and sysconfig_vars JSON to the new environment.
25+
data_name = sysconfig._get_sysconfigdata_name()
26+
json_name = data_name.replace("_sysconfigdata", "_sysconfig_vars")
27+
for filename in [
28+
"_cross_venv.pth",
29+
"_cross_venv.py",
30+
f"_cross_{sys.implementation._multiarch.replace('-', '_')}.py",
31+
f"{data_name}.py",
32+
f"{json_name}.json",
33+
]:
34+
src = SITE_PACKAGE_PATH / filename
35+
target = Path(path) / src.relative_to(
36+
SITE_PACKAGE_PATH.parent.parent.parent
37+
)
38+
if not target.exists():
39+
shutil.copy(src, target)
40+
return result
41+
42+
env._PipBackend.create = pip_env_create
43+
44+
45+
# Import hook that patches the creation of virtual environments by `build`
46+
#
47+
# The approach used here is the same as the one used by virtualenv to patch
48+
# distutils (but without support for the older load_module API).
49+
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
50+
_BUILD_PATCH = ("build.env",)
51+
52+
53+
class _Finder:
54+
"""A meta path finder that allows patching the imported build modules."""
55+
56+
fullname = None
57+
58+
# lock[0] is threading.Lock(), but initialized lazily to avoid importing
59+
# threading very early at startup, because there are gevent-based
60+
# applications that need to be first to import threading by themselves.
61+
# See https://github.com/pypa/virtualenv/issues/1895 for details.
62+
lock = [] # noqa: RUF012
63+
64+
def find_spec(self, fullname, path, target=None):
65+
if fullname in _BUILD_PATCH and self.fullname is None:
66+
# initialize lock[0] lazily
67+
if len(self.lock) == 0:
68+
import threading
69+
70+
lock = threading.Lock()
71+
# there is possibility that two threads T1 and T2 are
72+
# simultaneously running into find_spec, observing .lock as
73+
# empty, and further going into hereby initialization. However
74+
# due to the GIL, list.append() operation is atomic and this
75+
# way only one of the threads will "win" to put the lock
76+
# - that every thread will use - into .lock[0].
77+
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
78+
self.lock.append(lock)
79+
80+
from functools import partial
81+
from importlib.util import find_spec
82+
83+
with self.lock[0]:
84+
self.fullname = fullname
85+
try:
86+
spec = find_spec(fullname, path)
87+
if spec is not None:
88+
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
89+
old = spec.loader.exec_module
90+
func = self.exec_module
91+
if old is not func:
92+
spec.loader.exec_module = partial(func, old)
93+
return spec
94+
finally:
95+
self.fullname = None
96+
return None
97+
98+
@staticmethod
99+
def exec_module(old, module):
100+
old(module)
101+
if module.__name__ in _BUILD_PATCH:
102+
patch_env_create(module)
103+
104+
105+
sys.meta_path.insert(0, _Finder())

0 commit comments

Comments
 (0)