Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
40 changes: 26 additions & 14 deletions src/xbuild/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def _build(
config_settings: ConfigSettings | None,
skip_dependency_check: bool,
installer: _env.Installer,
sysconfig_path: Path | None,
build_details_path: Path | None,
sysconfigdata_path: Path | None,
) -> str:
if isolation:
return _build_in_isolated_env(
Expand All @@ -43,7 +44,8 @@ def _build(
distribution,
config_settings,
installer,
sysconfig_path=sysconfig_path,
build_details_path=build_details_path,
sysconfigdata_path=sysconfigdata_path,
)
else:
return _build_in_current_env(
Expand All @@ -61,11 +63,13 @@ def _build_in_isolated_env(
distribution: Distribution,
config_settings: ConfigSettings | None,
installer: _env.Installer,
sysconfig_path: Path | None,
build_details_path: Path | None,
sysconfigdata_path: Path | None,
) -> str:
with XBuildIsolatedEnv(
installer=installer,
sysconfig_path=sysconfig_path,
build_details_path=build_details_path,
sysconfigdata_path=sysconfigdata_path,
) as env:
builder = ProjectXBuilder.from_isolated_env(env, srcdir)

Expand Down Expand Up @@ -210,15 +214,24 @@ def main_parser() -> argparse.ArgumentParser:
help="Python package installer to use (defaults to pip)",
)

# This is only a required argument if the current environment isn't cross-compiling.
# If/when this project is merged into `build`, the existence of `--sysconfig` as
# an argument will be the trigger for "this is a cross platform build".
parser.add_argument(
# This is only a required argument if the current environment isn't
# cross-compiling. If/when this project is merged into `build`, the
# existence of `--build-details/--sysconfig` as an argument will be the
# trigger for "this is a cross platform build". The two arguments are
# mutually exclusive.
config_group = parser.add_mutually_exclusive_group(required=True)
config_group.add_argument(
"--build-details",
dest="build_details_path",
type=Path,
help=("The path to a build-details.json file.",),
)
config_group.add_argument(
"--sysconfig",
help="The path to a sysconfig_vars JSON file or sysconfigdata Python file",
required=not getattr(sys, "cross_compiling", False),
dest="sysconfigdata_path",
type=Path,
help=("The path to a sysconfigdata python file.",),
)

config_group = parser.add_mutually_exclusive_group()
config_group.add_argument(
"--config-setting",
Expand Down Expand Up @@ -290,8 +303,6 @@ def main(cli_args: Sequence[str], prog: str | None = None) -> None:
outdir = os.path.join(args.srcdir, "dist") if args.outdir is None else args.outdir

with _handle_build_error():
sysconfig_path = Path(args.sysconfig) if args.sysconfig else None

built = [
_build(
not args.no_isolation,
Expand All @@ -301,7 +312,8 @@ def main(cli_args: Sequence[str], prog: str | None = None) -> None:
config_settings,
args.skip_dependency_check,
args.installer,
sysconfig_path=sysconfig_path,
build_details_path=args.build_details_path,
sysconfigdata_path=args.sysconfigdata_path,
)
]
artifact_list = _natural_language_list(
Expand Down
17 changes: 11 additions & 6 deletions src/xbuild/env.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,13 @@ def install_requirements(
# and can handle both build and target dependency installs.
###########################################################################
class XBuildIsolatedEnv(DefaultIsolatedEnv):
def __init__(self, *, installer, sysconfig_path):
def __init__(self, *, installer, build_details_path, sysconfigdata_path):
if installer == "uv":
raise RuntimeError("Can't support uv (for now)")

super().__init__()
self.sysconfig_path = sysconfig_path
self.build_details_path = build_details_path
self.sysconfigdata_path = sysconfigdata_path

def __enter__(self) -> XBuildIsolatedEnv:
super().__enter__()
Expand All @@ -73,13 +74,17 @@ def __enter__(self) -> XBuildIsolatedEnv:
if not getattr(sys, "cross_compiling", False):
# We're in a local environment.
# Make the isolated environment a cross environment.
if self.sysconfig_path is None:
if self.build_details_path is None and self.sysconfigdata_path is None:
raise RuntimeError(
"Must specify the location of target platform sysconfig data "
"with --sysconfig"
"Must specify the location of target platform build_details.json "
"with --build-details, or sysconfigdata with --sysconfig"
)

convert_venv(Path(self._path), self.sysconfig_path)
convert_venv(
Path(self._path),
build_details_path=self.build_details_path,
sysconfigdata_path=self.sysconfigdata_path,
)
else:
# We're already in a cross environment.
# Copy any _cross_*.pth or _cross_*.py file, plus the cross-platform
Expand Down
31 changes: 26 additions & 5 deletions src/xvenv/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,23 @@ def main_parser():
default=0,
help="increase verbosity",
)
parser.add_argument(
"-s",

# Create mutually exclusive group for --build-details and --sysconfig
# One of these arguments must be provided
config_group = parser.add_mutually_exclusive_group(required=True)
config_group.add_argument(
"--build-details",
dest="build_details_path",
type=Path,
help=("The path to a build-details.json file.",),
)
config_group.add_argument(
"--sysconfig",
help="The path to a sysconfig_vars JSON file or sysconfigdata Python file",
dest="sysconfigdata_path",
type=Path,
help=("The path to a sysconfigdata python file.",),
)

parser.add_argument(
"venv",
help="The location of a native virtual environment",
Expand All @@ -60,13 +72,22 @@ def main(cli_args: Sequence[str], prog: str | None = None) -> None:
args = parser.parse_args(cli_args)

venv_path = Path(args.venv).resolve()
sysconfig_path = Path(args.sysconfig).resolve()
build_details_path = (
Path(args.build_details_path).resolve() if args.build_details_path else None
)
sysconfigdata_path = (
Path(args.sysconfigdata_path).resolve() if args.sysconfigdata_path else None
)

if not venv_path.exists():
_error(f"Native virtual environment {venv_path} does not exist.")
else:
try:
description = convert_venv(venv_path, sysconfig_path)
description = convert_venv(
venv_path,
build_details_path=build_details_path,
sysconfigdata_path=sysconfigdata_path,
)
except Exception as e:
_error(e)
sys.exit(1)
Expand Down
95 changes: 62 additions & 33 deletions src/xvenv/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import json
import pprint
import sys
from importlib import import_module
from importlib import util as importlib_util
from pathlib import Path
Expand Down Expand Up @@ -81,12 +80,18 @@ def localize_sysconfig_vars(sysconfig_vars_path, venv_site_packages):
return sysconfig_vars


def convert_venv(venv_path: Path, sysconfig_path: Path):
def convert_venv(
venv_path: Path,
build_details_path: Path | None,
sysconfigdata_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 to the sysconfig_vars JSON or sysconfigdata Python
file for the target platform.
:param build_details_path: The path to build-details.json file for the
target platform.
:param sysconfigdata_path: The path to the sysconfigdata python file
for the target platform.
"""
if not venv_path.exists():
raise ValueError(f"Virtual environment {venv_path} does not exist.")
Expand All @@ -102,42 +107,61 @@ def convert_venv(venv_path: Path, sysconfig_path: Path):

venv_site_packages_path = platlibs[0]

if not sysconfig_path.is_file():
raise ValueError(f"Could not find sysconfig file {sysconfig_path}")
if build_details_path:
if not build_details_path.is_file():
raise ValueError(f"Could not find {build_details_path}")

match sysconfig_path.suffix:
case ".json":
sysconfig_vars_path = sysconfig_path
_, _, _, abiflags, platform, multiarch = sysconfig_vars_path.stem.split("_")
# If build_details.json exists, then so does sysconfig_vars.
with open(build_details_path) as fp:
build_details = json.load(fp)

sysconfigdata_path = (
sysconfig_vars_path.parent
# build_details platform is the full platform-min_version-multiarch
# format. We only need the platform part.
platform = build_details["platform"].split("-")[0]
version = build_details["language"]["version"]
abiflags = "".join(build_details["abi"]["flags"])
multiarch = build_details["implementation"]["_multiarch"]

localize_sysconfigdata(
(
build_details_path.parent
/ f"_sysconfigdata_{abiflags}_{platform}_{multiarch}.py"
)
case ".py":
sysconfigdata_path = sysconfig_path
_, _, abiflags, platform, multiarch = sysconfigdata_path.stem.split("_")
),
venv_site_packages_path,
)
localize_sysconfig_vars(
(
build_details_path.parent
/ f"_sysconfig_vars_{abiflags}_{platform}_{multiarch}.json"
),
venv_site_packages_path,
)
elif sysconfigdata_path:
if not sysconfigdata_path.is_file():
raise ValueError(f"Could not find {sysconfigdata_path}")

sysconfig_vars_path = (
sysconfigdata_path.parent
/ f"_sysconfig_vars_{abiflags}_{platform}_{multiarch}.py"
)
case _:
raise ValueError(
"Don't know how to process sysconfig data "
f"of type {sysconfig_path.suffix}"
)
# If we've been given a sysconfigdata file, re
_, _, abiflags, platform, multiarch = sysconfigdata_path.stem.split("_", 4)

# Localize the sysconfig data. sysconfigdata *must* exist; sysconfig_vars
# will only exist on Python 3.14 or newer.
sysconfig = localize_sysconfigdata(sysconfigdata_path, venv_site_packages_path)
if sys.version_info[:2] >= (3, 14):
localize_sysconfig_vars(sysconfig_vars_path, venv_site_packages_path)
# Localize the sysconfig data.
sysconfigdata = localize_sysconfigdata(
sysconfigdata_path,
venv_site_packages_path,
)
version = sysconfigdata["VERSION"]

# We'll need to reconstruct build_details-like data once we have
# a platform module, as the keys in sysconfig data vary by platform.
build_details = None
else:
raise ValueError(
"Must provide path to either build_details.json or sysconfigdata"
)

if sysconfig["VERSION"] != venv_site_packages_path.parts[-2][6:]:
if version != venv_site_packages_path.parts[-2][6:]:
raise ValueError(
f"target venv is Python {venv_site_packages_path.parts[-2][6:]}; "
f"sysconfig file is for Python {sysconfig['VERSION']}"
f"build details file is for Python {version}"
)

# Generate the context for the templated cross-target file
Expand All @@ -153,7 +177,12 @@ def convert_venv(venv_path: Path, sysconfig_path: Path):

try:
platform_module = import_module(f"xvenv.platforms.{platform}")
platform_module.extend_context(context, sysconfig)
if build_details is None:
build_details = platform_module.build_details_from_sysconfigdata(
sysconfigdata
)

platform_module.extend_context(context, build_details)
except ImportError:
raise ValueError(
f"Don't know how to build a cross-venv file for {platform}"
Expand Down
14 changes: 12 additions & 2 deletions src/xvenv/platforms/android.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
def extend_context(context, sysconfig):
def build_details_from_sysconfigdata(sysconfigdata):
# Reconstruct a build_details-alike structure from sysconfigdata.
arch, _, platform = sysconfigdata["MULTIARCH"].split("-")
min_api_level = sysconfigdata["ANDROID_API_LEVEL"]
build_details = {
"platform": f"{platform}-{min_api_level}-{arch}",
}
return build_details


def extend_context(context, build_details):
# Convert the API level into a release number
api_level = int(sysconfig["ANDROID_API_LEVEL"])
api_level = int(build_details["platform"].split("-")[1])
if api_level >= 33:
release = f"{api_level - 20}"
elif api_level == 32:
Expand Down
10 changes: 9 additions & 1 deletion src/xvenv/platforms/emscripten.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
def extend_context(context, sysconfig):
def build_details_from_sysconfigdata(sysconfigdata):
# Reconstruct a build_details-alike structure from sysconfigdata.
build_details = {
"platform": "emscripten-4.0.12-wasm32",
}
return build_details


def extend_context(context, build_details):
emscripten_version = "4.0.12"
context["release"] = emscripten_version
context["platform_version"] = emscripten_version
Expand Down
15 changes: 13 additions & 2 deletions src/xvenv/platforms/ios.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
def extend_context(context, sysconfig):
release = sysconfig["IPHONEOS_DEPLOYMENT_TARGET"]
def build_details_from_sysconfigdata(sysconfigdata):
# Reconstruct a build_details-alike structure from sysconfigdata.
platform = sysconfigdata["MACHDEP"]
multiarch = sysconfigdata["MULTIARCH"]
ios_target = sysconfigdata["IPHONEOS_DEPLOYMENT_TARGET"]
build_details = {
"platform": f"{platform}-{ios_target}-{multiarch}",
}
return build_details


def extend_context(context, build_details):
release = build_details["platform"].split("-")[1]
is_simulator = context["sdk"] == "iphonesimulator"

######################################################################
Expand Down