* feat: add CPython 3.15 support for iOS Assisted-by: OpenCode:Kimi-K2.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: copy in iOS support files Assisted-by: Copilot:claude-sonnet-4.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: skip directories when copying iOS support files When copying multiarch-specific support files (e.g. _cross_arm64_iphoneos.py), the code was trying to copy all items in the directory including __pycache__ directories. The shutil.copy() function only works with files, not directories, which caused an IsADirectoryError. This fix adds a check to only copy files, skipping any directories like __pycache__ that may have been created by Python imports. Assisted-by: Copilot:claude-haiku-4.5 * fix: - in dir fine for now Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: apply review suggestions Assisted-by: Copilot:claude-sonnet-4.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * fix: only do this on 3.15+ Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * Ensure target Python directory exists before build Add assertion to check if target Python directory exists. * fix(ios): use stdlib dir directly instead of copying to platform-config Remove the _inject_support_files workaround for python.org 3.15+ distributions. Instead of copying sysconfig files from the stdlib into a synthetic platform-config/ directory, pass the stdlib directory directly to make_cross_venv.py. make_cross_venv.py is updated to derive the multiarch tag from the _sysconfigdata_ filename rather than assuming it comes from the directory name. Co-authored-by: Russell Keith-Magee <russell@keith-magee.com> Assisted-by: OpenCode:Kimi-K2.6 Signed-off-by: Henry Schreiner <henryfs@princeton.edu> * Minor format cleanups on pre-commit skipped files. * Clarify path names in make_cross_venv script. --------- Signed-off-by: Henry Schreiner <henryfs@princeton.edu> Co-authored-by: Russell Keith-Magee <russell@keith-magee.com>
176 lines
6.8 KiB
Python
176 lines
6.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import pprint
|
|
import shutil
|
|
import sys
|
|
from importlib import util as importlib_util
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
def localized_vars(orig_vars: dict[str, Any], slice_path: Path) -> dict[str, Any]:
|
|
"""Update (where possible) any references to build-time variables with the
|
|
best guess of the installed location.
|
|
"""
|
|
# The host's sysconfigdata will include references to build-time variables.
|
|
# Update these to refer to the current known install location.
|
|
orig_prefix = orig_vars["prefix"]
|
|
localized_vars = {}
|
|
for key, value in orig_vars.items():
|
|
final = value
|
|
if isinstance(value, str):
|
|
# Replace any reference to the build installation prefix
|
|
final = final.replace(orig_prefix, str(slice_path))
|
|
# Replace any reference to the build-time Framework location
|
|
final = final.replace("-F .", f"-F {slice_path}")
|
|
localized_vars[key] = final
|
|
|
|
return localized_vars
|
|
|
|
|
|
def localize_sysconfigdata(sysconfig_path: Path, venv_site_packages: Path) -> None:
|
|
"""Localize a sysconfigdata python module.
|
|
|
|
:param sysconfig_path: The platform config that contains the
|
|
sysconfigdata module to localize.
|
|
:param venv_site_packages: The site packages folder where the localized
|
|
sysconfigdata module should be output.
|
|
"""
|
|
# Find the "_sysconfigdata_*.py" file in the platform config
|
|
sysconfigdata_path = next(sysconfig_path.glob("_sysconfigdata_*.py"))
|
|
|
|
# Import the sysconfigdata module
|
|
spec = importlib_util.spec_from_file_location(sysconfigdata_path.stem, sysconfigdata_path)
|
|
if spec is None:
|
|
msg = f"Unable to load spec for {sysconfigdata_path}"
|
|
raise ValueError(msg)
|
|
if spec.loader is None:
|
|
msg = f"Spec for {sysconfigdata_path} does not define a loader"
|
|
raise ValueError(msg)
|
|
sysconfigdata = importlib_util.module_from_spec(spec)
|
|
spec.loader.exec_module(sysconfigdata)
|
|
|
|
# Write the updated sysconfigdata module into the cross-platform site.
|
|
slice_path = sysconfigdata_path.parent.parent.parent
|
|
with (venv_site_packages / sysconfigdata_path.name).open("w") as f:
|
|
f.write(f"# Generated from {sysconfigdata_path}\n")
|
|
f.write("build_time_vars = ")
|
|
pprint.pprint(
|
|
localized_vars(sysconfigdata.build_time_vars, slice_path), stream=f, compact=True
|
|
)
|
|
|
|
|
|
def localize_sysconfig_vars(sysconfig_path: Path, venv_site_packages: Path) -> None:
|
|
"""Localize a sysconfig_vars.json file.
|
|
|
|
:param sysconfig_path: The platform config that contains the
|
|
sysconfigdata module to localize.
|
|
:param venv_site_packages: The site-packages folder where the localized
|
|
sysconfig_vars.json file should be output.
|
|
"""
|
|
# Find the "_sysconfig_vars_*.json" file in the platform config
|
|
sysconfig_vars_path = next(sysconfig_path.glob("_sysconfig_vars_*.json"))
|
|
|
|
with sysconfig_vars_path.open("rb") as f:
|
|
build_time_vars = json.load(f)
|
|
|
|
slice_path = sysconfig_vars_path.parent.parent.parent
|
|
with (venv_site_packages / sysconfig_vars_path.name).open("w") as f:
|
|
json.dump(localized_vars(build_time_vars, slice_path), f, indent=2)
|
|
|
|
|
|
def make_cross_venv(
|
|
venv_path: Path,
|
|
sysconfig_path: Path,
|
|
platform_config_path: Path,
|
|
) -> None:
|
|
"""Convert a virtual environment into a cross-platform environment.
|
|
|
|
:param venv_path: The path to the root of the venv.
|
|
:param sysconfig_path: The path containing sysconfigdata files for the
|
|
target platform.
|
|
:param platform_config_path: The path containing the cross-venv support
|
|
scripts (_cross_venv.py and the multiarch-specific _cross_*.py).
|
|
"""
|
|
if not venv_path.exists():
|
|
msg = f"Virtual environment {venv_path} does not exist."
|
|
raise ValueError(msg)
|
|
if not (venv_path / "bin/python3").exists():
|
|
msg = f"{venv_path} does not appear to be a virtual environment."
|
|
raise ValueError(msg)
|
|
|
|
LIB_PATH = f"lib/python{sys.version_info[0]}.{sys.version_info[1]}"
|
|
|
|
# Derive the multiarch tag from the sysconfigdata filename if available;
|
|
# otherwise fall back to the directory name (BeeWare convention).
|
|
sysconfigdata_files = list(sysconfig_path.glob("_sysconfigdata_*.py"))
|
|
if sysconfigdata_files:
|
|
multiarch_tag = sysconfigdata_files[0].stem.split("_ios_")[1]
|
|
cross_multiarch = f"_cross_{multiarch_tag.replace('-', '_')}"
|
|
else:
|
|
multiarch_tag = sysconfig_path.name
|
|
cross_multiarch = f"_cross_{multiarch_tag.replace('-', '_')}"
|
|
|
|
print(
|
|
f"Converting {venv_path} into a {multiarch_tag} environment... ",
|
|
end="",
|
|
)
|
|
|
|
# Update path references in the sysconfigdata to reflect local conditions.
|
|
venv_site_packages = venv_path / LIB_PATH / "site-packages"
|
|
localize_sysconfigdata(sysconfig_path, venv_site_packages)
|
|
localize_sysconfig_vars(sysconfig_path, venv_site_packages)
|
|
# The multiarch-specific script may be directly in platform_config_path (BeeWare
|
|
# convention, where all files are co-located) or in a named subdirectory
|
|
# (cibuildwheel resource layout, where each multiarch has its own subdir).
|
|
multiarch_script = platform_config_path / f"{cross_multiarch}.py"
|
|
if not multiarch_script.exists():
|
|
multiarch_script = platform_config_path / multiarch_tag / f"{cross_multiarch}.py"
|
|
shutil.copy(
|
|
multiarch_script,
|
|
venv_site_packages / f"{cross_multiarch}.py",
|
|
)
|
|
shutil.copy(
|
|
platform_config_path / "_cross_venv.py",
|
|
venv_site_packages / "_cross_venv.py",
|
|
)
|
|
# Write the .pth file that will enable the cross-env modifications
|
|
(venv_site_packages / "_cross_venv.pth").write_text(
|
|
f"import {cross_multiarch}; import _cross_venv\n"
|
|
)
|
|
|
|
print("done.")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
sysconfig_path = Path(sys.argv[2]).resolve()
|
|
except IndexError:
|
|
sysconfig_path = Path(__file__).parent
|
|
|
|
try:
|
|
platform_config_path = Path(sys.argv[3]).resolve()
|
|
except IndexError:
|
|
platform_config_path = sysconfig_path
|
|
|
|
try:
|
|
venv_path = Path(sys.argv[1]).resolve()
|
|
make_cross_venv(venv_path, sysconfig_path, platform_config_path)
|
|
except IndexError:
|
|
print("""
|
|
Convert a virtual environment into a cross-platform environment.
|
|
|
|
Usage:
|
|
make_cross_venv <venv> (<sysconfig path>) (<platform config path>)
|
|
|
|
The sysconfig path is the path that contains the `_sysconfigdata-*.py` file
|
|
for the platform being targeted. If an explicit sysconfig path isn't
|
|
provided, it is assumed the directory containing the make_cross_venv script
|
|
also contains the sysconfig data for the interpreter.
|
|
|
|
The platform config path is the path that contains the cross-venv support
|
|
scripts. If an explicit platform config path isn't provided, it is assumed
|
|
that the sysconfig path also contains the platform configuration files.
|
|
""")
|