diff --git a/src/xbuild/__main__.py b/src/xbuild/__main__.py index d378a0e..ad5b234 100644 --- a/src/xbuild/__main__.py +++ b/src/xbuild/__main__.py @@ -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( @@ -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( @@ -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) @@ -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", @@ -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, @@ -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( diff --git a/src/xbuild/env.py b/src/xbuild/env.py index a44f46c..b5e638a 100644 --- a/src/xbuild/env.py +++ b/src/xbuild/env.py @@ -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__() @@ -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 diff --git a/src/xvenv/__main__.py b/src/xvenv/__main__.py index 1c48c35..bb74fdc 100644 --- a/src/xvenv/__main__.py +++ b/src/xvenv/__main__.py @@ -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", @@ -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) diff --git a/src/xvenv/convert.py b/src/xvenv/convert.py index 4b4bdd6..3888f62 100644 --- a/src/xvenv/convert.py +++ b/src/xvenv/convert.py @@ -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 @@ -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.") @@ -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 @@ -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}" diff --git a/src/xvenv/platforms/android.py b/src/xvenv/platforms/android.py index 358cba1..4ab5a4b 100644 --- a/src/xvenv/platforms/android.py +++ b/src/xvenv/platforms/android.py @@ -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: diff --git a/src/xvenv/platforms/emscripten.py b/src/xvenv/platforms/emscripten.py index 0efb8fb..9afeeb6 100644 --- a/src/xvenv/platforms/emscripten.py +++ b/src/xvenv/platforms/emscripten.py @@ -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 diff --git a/src/xvenv/platforms/ios.py b/src/xvenv/platforms/ios.py index c699f4b..71f9c29 100644 --- a/src/xvenv/platforms/ios.py +++ b/src/xvenv/platforms/ios.py @@ -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" ######################################################################