xbuild 0.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
xbuild/__init__.py ADDED
@@ -0,0 +1,20 @@
1
+ __all__ = [
2
+ "__version__",
3
+ ]
4
+
5
+ try:
6
+ # Read version from SCM metadata
7
+ # This will only exist in a development environment
8
+ from setuptools_scm import get_version
9
+
10
+ # Excluded from coverage because a pure test environment (such as the one
11
+ # used by tox in CI) won't have setuptools_scm
12
+ __version__ = get_version("../..", relative_to=__file__) # pragma: no cover
13
+ except (ModuleNotFoundError, LookupError): # pragma: no-cover-if-missing-setuptools_scm
14
+ # If setuptools_scm isn't in the environment, the call to import will fail.
15
+ # If it *is* in the environment, but the code isn't a git checkout (e.g.,
16
+ # it's been pip installed non-editable) the call to get_version() will fail.
17
+ # If either of these occurs, read version from the installer metadata.
18
+ from importlib.metadata import version
19
+
20
+ __version__ = version("xbuild")
xbuild/__main__.py ADDED
@@ -0,0 +1,224 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import os
5
+ import sys
6
+ from collections.abc import Sequence
7
+ from functools import partial
8
+
9
+ from build import ProjectBuilder
10
+ from build import env as _env
11
+ from build.__main__ import (
12
+ _cprint,
13
+ _handle_build_error,
14
+ _max_terminal_width,
15
+ _natural_language_list,
16
+ _setup_cli,
17
+ _styles,
18
+ )
19
+ from build._types import ConfigSettings, Distribution, StrPath
20
+ from build.env import DefaultIsolatedEnv
21
+
22
+ import xbuild
23
+
24
+
25
+ def _build_in_isolated_env(
26
+ srcdir: StrPath,
27
+ outdir: StrPath,
28
+ distribution: Distribution,
29
+ config_settings: ConfigSettings | None,
30
+ installer: _env.Installer,
31
+ ) -> str:
32
+ with DefaultIsolatedEnv(installer=installer) as env:
33
+ builder = ProjectBuilder.from_isolated_env(env, srcdir)
34
+ # first install the build dependencies
35
+ env.install(builder.build_system_requires)
36
+ # then get the extra required dependencies from the backend
37
+ # (which was installed in the call above :P)
38
+ env.install(builder.get_requires_for_build(distribution, config_settings or {}))
39
+ return builder.build(distribution, outdir, config_settings or {})
40
+
41
+
42
+ def main_parser() -> argparse.ArgumentParser:
43
+ """Construct the main parser."""
44
+ formatter_class = partial(
45
+ argparse.RawDescriptionHelpFormatter, width=min(_max_terminal_width, 127)
46
+ )
47
+ # Workaround for 3.14.0 beta 1, can remove once beta 2 is out
48
+ if sys.version_info >= (3, 14):
49
+ formatter_class = partial(formatter_class, color=True)
50
+
51
+ make_parser = partial(
52
+ argparse.ArgumentParser,
53
+ description="A cross-platform build backend for Python",
54
+ # Prevent argparse from taking up the entire width of the terminal window
55
+ # which impedes readability. Also keep the description formatted.
56
+ formatter_class=formatter_class,
57
+ )
58
+ if sys.version_info >= (3, 14):
59
+ make_parser = partial(make_parser, suggest_on_error=True, color=True)
60
+
61
+ parser = make_parser()
62
+ parser.add_argument(
63
+ "srcdir",
64
+ type=str,
65
+ nargs="?",
66
+ default=os.getcwd(),
67
+ help="source directory (defaults to current directory)",
68
+ )
69
+ parser.add_argument(
70
+ "--version",
71
+ "-V",
72
+ action="version",
73
+ version=f"xbuild {xbuild.__version__} ({','.join(xbuild.__path__)})",
74
+ )
75
+ parser.add_argument(
76
+ "--verbose",
77
+ "-v",
78
+ dest="verbosity",
79
+ action="count",
80
+ default=0,
81
+ help="increase verbosity",
82
+ )
83
+ # parser.add_argument(
84
+ # "--sdist",
85
+ # "-s",
86
+ # dest="distributions",
87
+ # action="append_const",
88
+ # const="sdist",
89
+ # help="build a source distribution (disables the default behavior)",
90
+ # )
91
+ # parser.add_argument(
92
+ # "--wheel",
93
+ # "-w",
94
+ # dest="distributions",
95
+ # action="append_const",
96
+ # const="wheel",
97
+ # help="build a wheel (disables the default behavior)",
98
+ # )
99
+ parser.add_argument(
100
+ "--outdir",
101
+ "-o",
102
+ type=str,
103
+ help=f"output directory (defaults to {{srcdir}}{os.sep}dist)",
104
+ metavar="PATH",
105
+ )
106
+ # parser.add_argument(
107
+ # "--skip-dependency-check",
108
+ # "-x",
109
+ # action="store_true",
110
+ # help="do not check that build dependencies are installed",
111
+ # )
112
+ # env_group = parser.add_mutually_exclusive_group()
113
+ # env_group.add_argument(
114
+ # "--no-isolation",
115
+ # "-n",
116
+ # action="store_true",
117
+ # help="disable building the project in an isolated virtual environment. "
118
+ # "Build dependencies must be installed separately when this option is used",
119
+ # )
120
+ # env_group.add_argument(
121
+ # "--installer",
122
+ # choices=_env.INSTALLERS,
123
+ # help="Python package installer to use (defaults to pip)",
124
+ # )
125
+ # config_group = parser.add_mutually_exclusive_group()
126
+ # config_group.add_argument(
127
+ # "--config-setting",
128
+ # "-C",
129
+ # dest="config_settings",
130
+ # action="append",
131
+ # help=(
132
+ # "settings to pass to the backend. Multiple settings can be "
133
+ # "provided. Settings beginning with a hyphen will erroneously "
134
+ # "be interpreted as options to build if separated by a space "
135
+ # "character; use ``--config-setting=--my-setting -C--my-other-setting``"
136
+ # ),
137
+ # metavar="KEY[=VALUE]",
138
+ # )
139
+ # config_group.add_argument(
140
+ # "--config-json",
141
+ # dest="config_json",
142
+ # help=(
143
+ # "settings to pass to the backend as a JSON object. This is an "
144
+ # "alternative to --config-setting that allows complex nested "
145
+ # "structures. Cannot be used together with --config-setting"
146
+ # ),
147
+ # metavar="JSON_STRING",
148
+ # )
149
+
150
+ return parser
151
+
152
+
153
+ def main(cli_args: Sequence[str], prog: str | None = None) -> None:
154
+ """Parse the CLI arguments and invoke the build process.
155
+
156
+ :param cli_args: CLI arguments
157
+ :param prog: Program name to show in help text
158
+ """
159
+ parser = main_parser()
160
+ if prog:
161
+ parser.prog = prog
162
+ args = parser.parse_args(cli_args)
163
+
164
+ _setup_cli(verbosity=args.verbosity)
165
+
166
+ config_settings = {}
167
+
168
+ # # Handle --config-json
169
+ # if args.config_json:
170
+ # try:
171
+ # config_settings = json.loads(args.config_json)
172
+ # if not isinstance(config_settings, dict):
173
+ # _error(
174
+ # "--config-json must contain a JSON object (dict), "
175
+ # "not a list or primitive value"
176
+ # )
177
+ # except json.JSONDecodeError as e:
178
+ # _error(f"Invalid JSON in --config-json: {e}")
179
+
180
+ # # Handle --config-setting (original logic)
181
+ # elif args.config_settings:
182
+ # for arg in args.config_settings:
183
+ # setting, _, value = arg.partition("=")
184
+ # if setting not in config_settings:
185
+ # config_settings[setting] = value
186
+ # else:
187
+ # if not isinstance(config_settings[setting], list):
188
+ # config_settings[setting] = [config_settings[setting]]
189
+
190
+ # config_settings[setting].append(value)
191
+
192
+ # outdir is relative to srcdir only if omitted.
193
+ outdir = os.path.join(args.srcdir, "dist") if args.outdir is None else args.outdir
194
+
195
+ with _handle_build_error():
196
+ built = [
197
+ _build_in_isolated_env(
198
+ args.srcdir,
199
+ outdir,
200
+ "wheel",
201
+ config_settings,
202
+ "pip",
203
+ )
204
+ ]
205
+ artifact_list = _natural_language_list(
206
+ [
207
+ "{underline}{}{reset}{bold}{green}".format(artifact, **_styles.get())
208
+ for artifact in built
209
+ ]
210
+ )
211
+ _cprint("{bold}{green}Successfully built {}{reset}", artifact_list)
212
+
213
+
214
+ def entrypoint() -> None:
215
+ main(sys.argv[1:])
216
+
217
+
218
+ if __name__ == "__main__": # pragma: no cover
219
+ main(sys.argv[1:], "python -m build")
220
+
221
+ __all__ = [
222
+ "main",
223
+ "main_parser",
224
+ ]
@@ -0,0 +1,99 @@
1
+ Metadata-Version: 2.4
2
+ Name: xbuild
3
+ Version: 0.0.1
4
+ Summary: A PEP517 build frontend for cross-compiling wheels.
5
+ Author-email: Russell Keith-Magee <russell@keith-magee.com>
6
+ Maintainer-email: Russell Keith-Magee <russell@keith-magee.com>
7
+ License-Expression: BSD-3-Clause
8
+ Project-URL: Project, https://github.com/beeware/xbuild
9
+ Project-URL: Funding, https://beeware.org/contributing/membership/
10
+ Project-URL: Tracker, https://github.com/beeware/xbuild/issues
11
+ Project-URL: Source, https://github.com/beeware/xbuild
12
+ Keywords: pep517,build
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Classifier: Topic :: System :: Software Distribution
21
+ Classifier: Topic :: Software Development
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.13
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Requires-Dist: build==1.3.0
27
+ Dynamic: license-file
28
+
29
+ # xbuild
30
+
31
+ [![Python Versions](https://img.shields.io/pypi/pyversions/xbuild.svg)](https://pypi.python.org/pypi/xbuild)
32
+ [![PyPI Version](https://img.shields.io/pypi/v/xbuild.svg)](https://pypi.python.org/pypi/xbuild)
33
+ [![Maturity](https://img.shields.io/pypi/status/xbuild.svg)](https://pypi.python.org/pypi/xbuild)
34
+ [![BSD License](https://img.shields.io/pypi/l/xbuild.svg)](https://github.com/beeware/xbuild/blob/master/LICENSE)
35
+ [![Discord server](https://img.shields.io/discord/836455665257021440?label=Discord%20Chat&logo=discord&style=plastic)](https://beeware.org/bee/chat/)
36
+
37
+ `xbuild` is PEP517 build frontend that has additions and extensions to support cross-compiling wheels for platforms where compilation cannot be performed natively - most notably:
38
+
39
+ * Android
40
+ * Emscripten (WASM)
41
+ * iOS
42
+
43
+ ## Usage
44
+
45
+ ### Creating a cross virtual environment
46
+
47
+ To create a cross virtual environment, you will need a distribution of Python that has been compiled for Android, Emscripten or iOS. Create a virtual environment for your build platform (i.e., the platform where you will be compiling), then use the `xvenv` script to convert that virtual environment in to a cross environment.
48
+
49
+ $ python3 -m venv venv
50
+ $ source venv/bin/activate
51
+ (venv) $ pip install xbuild
52
+ (venv) $ python -m venv x-venv
53
+ (venv) $ xvenv --sysconfig path/to/_sysconfig_vars__...json x-venv
54
+
55
+ You can then activate the cross virtual environment. For example, if `x-venv` was constructed using an iOS simulator sysconfig vars file (`_sysconfig_vars__ios_arm64-iphonesimulator.json`), you would see output like:
56
+
57
+ $ source x-venv/bin/activate
58
+ (x-venv) $ python -c "import sys; print(sys.platform)"
59
+ ios
60
+ (x-venv) $ python -c "import sys; print(sys.implementation._multiarch)"
61
+ arm64-iphonesimulator
62
+
63
+ This should now print the platform identifier for the target platform, not your build platform.
64
+
65
+ You can also configure xvenv with a `_sysconfigdata` Python file (e.g., `_sysconfigdata__ios_arm64-iphonesimulator.py`), instead of the `_sysconfig_var` JSON file. You'll have to use `_sysconfigdata` if you're on Python 3.13 (as the JSON format was only introduced in Python 3.14)
66
+
67
+ If you are in the cross environment, and you need to temporarily convert it back to the build platform, you can do so with the `XBUILD_ENV` environment variable. For example, if `x-venv` is an iOS cross environment:
68
+
69
+ $ source x-venv/bin/activate
70
+ (x-venv) $ python -c "import sys; print(sys.platform)"
71
+ ios
72
+ (x-venv) $ XBUILD_ENV=off python -c "import sys; print(sys.platform)"
73
+ darwin
74
+
75
+ ## How this works
76
+
77
+ The cross build environment does not run the target platform binaries on the build platform - it uses binaries for the build platform, but monkey-patches the Python interpreter at startup so that any question asking details about the platform returns details about the target platform. For example, if you create an iOS cross-platform environment on a macOS machine, you'll be using the macOS `python.exe`; but if you ask for `sys.platform`, the answer will be `ios`, not `darwin`.
78
+
79
+ ## Contributing
80
+
81
+ To set up a development environment:
82
+
83
+ $ python3 -m venv venv
84
+ $ source venv/bin/activate
85
+ (venv) $ python -m pip install -U pip
86
+ (venv) $ python -m pip install -e . --group dev
87
+
88
+ ## Community
89
+
90
+ `xbuild` is part of the [BeeWare suite](http://beeware.org). You can talk to the community through:
91
+
92
+ - [@pybeeware on Twitter](https://twitter.com/pybeeware)
93
+ - [Discord](https://beeware.org/bee/chat/)
94
+
95
+ We foster a welcoming and respectful community as described in our [BeeWare Community Code of Conduct](http://beeware.org/community/behavior/).
96
+
97
+ ## Contributing
98
+
99
+ If you experience problems with `xbuild`, [log them on GitHub](https://github.com/beeware/xbuild/issues). If you want to contribute code, please [fork the code](https://github.com/beeware/xbuild) and [submit a pull request](https://github.com/beeware/xbuild/pulls).
@@ -0,0 +1,16 @@
1
+ xbuild/__init__.py,sha256=u3wDO6aPZzsZgXih4JqrDNHlgZ0SHvHDaYCy24P8wTg,869
2
+ xbuild/__main__.py,sha256=PeaMnXoI4aOHXdkPcTh1WIvssuRSXt_62eXR25L6lM8,7083
3
+ xbuild-0.0.1.dist-info/licenses/LICENSE,sha256=XYQSYHFcE30mh1LcQccvGSWMBfYnRdvEZoleqfnUios,1522
4
+ xvenv/__init__.py,sha256=6jjRnZghwSf0mG6KbapDKm1WJUDWBwv7Ro7GukzSCnY,103
5
+ xvenv/__main__.py,sha256=eppmUqYjKlVl80023Y849Wceq11WTec0Kr5XPhmDqvM,2094
6
+ xvenv/_cross_target.py.tmpl,sha256=CHcZ3-P7j-VGZBaG2gFU8QJmFwY4ssKVk0UYOBZrOnE,3247
7
+ xvenv/convert.py,sha256=UJDhOgfiqQ0z-Ddy_0GzgcYFSs4F_WXAg7shSwQyoXI,6698
8
+ xvenv/platforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ xvenv/platforms/android.py,sha256=cmjRo3oekxnImFiRiPvElT6AjKlWDzL7R-W7KJQHnmc,2282
10
+ xvenv/platforms/emscripten.py,sha256=TJjUl5yiPhuqloiWuEM9b2py7N6JlQZ80CXbBJ8JB1E,526
11
+ xvenv/platforms/ios.py,sha256=qcK9DD7QcJsI6xrLB59EqD9T6yUA_kGov6v33Hk025k,1205
12
+ xbuild-0.0.1.dist-info/METADATA,sha256=FL0cDFumxqXsfqzxhxq0U3N-T1jwQ-CTDcjQUKwNVb8,5086
13
+ xbuild-0.0.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
14
+ xbuild-0.0.1.dist-info/entry_points.txt,sha256=GIHORBLMolwW-AkaAFdDP6-3rks6u6V8d_ZpTWy_K9Q,88
15
+ xbuild-0.0.1.dist-info/top_level.txt,sha256=vI5VC2agCGAoVSufMhr23Z0E9DwwK--TDsxcyRh5IVc,13
16
+ xbuild-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ xbuild = xbuild.__main__:entrypoint
3
+ xvenv = xvenv.__main__:entrypoint
@@ -0,0 +1,27 @@
1
+ Copyright (c) 2025 Russell Keith-Magee.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ 1. Redistributions of source code must retain the above copyright notice,
8
+ this list of conditions and the following disclaimer.
9
+
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in the
12
+ documentation and/or other materials provided with the distribution.
13
+
14
+ 3. Neither the name of xbuild nor the names of its
15
+ contributors may be used to endorse or promote products derived from this
16
+ software without specific prior written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
19
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
22
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
25
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,2 @@
1
+ xbuild
2
+ xvenv
xvenv/__init__.py ADDED
@@ -0,0 +1,6 @@
1
+ # xvenv version is the same as xbuild
2
+ from xbuild import __version__
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ ]
xvenv/__main__.py ADDED
@@ -0,0 +1,90 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from argparse import ArgumentParser
5
+ from collections.abc import Sequence
6
+ from pathlib import Path
7
+
8
+ from build.__main__ import (
9
+ _cprint,
10
+ _error,
11
+ )
12
+
13
+ import xvenv
14
+ from xvenv.convert import convert_venv
15
+
16
+
17
+ def main_parser():
18
+ parser = ArgumentParser(
19
+ "xvenv",
20
+ description=(
21
+ "Convert a native virtual environment into a cross-platform "
22
+ "virtual environment."
23
+ ),
24
+ )
25
+ parser.add_argument(
26
+ "--version",
27
+ "-V",
28
+ action="version",
29
+ version=f"xvenv {xvenv.__version__}",
30
+ )
31
+ parser.add_argument(
32
+ "--verbose",
33
+ "-v",
34
+ dest="verbosity",
35
+ action="count",
36
+ default=0,
37
+ help="increase verbosity",
38
+ )
39
+ parser.add_argument(
40
+ "-s",
41
+ "--sysconfig",
42
+ help="The path to a sysconfig_vars JSON file or sysconfigdata Python file",
43
+ )
44
+ parser.add_argument(
45
+ "venv",
46
+ help="The location of a native virtual environment",
47
+ )
48
+ return parser
49
+
50
+
51
+ def main(cli_args: Sequence[str], prog: str | None = None) -> None:
52
+ """Parse the CLI arguments and convert the venv.
53
+
54
+ :param cli_args: CLI arguments
55
+ :param prog: Program name to show in help text
56
+ """
57
+ parser = main_parser()
58
+ if prog:
59
+ parser.prog = prog
60
+ args = parser.parse_args(cli_args)
61
+
62
+ venv_path = Path(args.venv).resolve()
63
+ sysconfig_path = Path(args.sysconfig).resolve()
64
+
65
+ if not venv_path.exists():
66
+ _error(f"Native virtual environment {venv_path} does not exist.")
67
+ else:
68
+ try:
69
+ description = convert_venv(venv_path, sysconfig_path)
70
+ except Exception as e:
71
+ _error(e)
72
+ sys.exit(1)
73
+ else:
74
+ _cprint(
75
+ "{bold}{green}{}{reset}",
76
+ f"{args.venv} is now an {description} cross venv.",
77
+ )
78
+
79
+
80
+ def entrypoint() -> None:
81
+ main(sys.argv[1:])
82
+
83
+
84
+ if __name__ == "__main__": # pragma: no cover
85
+ main(sys.argv[1:], "python -m build")
86
+
87
+ __all__ = [
88
+ "main",
89
+ "main_parser",
90
+ ]
@@ -0,0 +1,95 @@
1
+ # A site package that turns a virtual environment
2
+ # into a {os} {multiarch} cross-platform virtual environment
3
+ import functools
4
+ import os
5
+ import platform
6
+ import subprocess
7
+ import sys
8
+ import sysconfig
9
+
10
+
11
+ def monkeypatch(module):
12
+ """A decorator for patching a function in a module."""
13
+ def _dec(fn):
14
+ orig_fn = getattr(module, fn.__name__, None)
15
+ setattr(module, fn.__name__, fn)
16
+ functools.wraps(orig_fn)(fn)
17
+
18
+ return _dec
19
+
20
+
21
+ def patch_cross_env():
22
+ ###########################################################################
23
+ # sys module patches
24
+ ###########################################################################
25
+ sys.cross_compiling = True
26
+ sys.platform = "{platform}"
27
+ sys.implementation._multiarch = "{multiarch}"
28
+ sys.abiflags = "{abiflags}"
29
+
30
+ if sys.version_info < (3, 14):
31
+ sys.base_prefix = sysconfig.get_config_var("prefix")
32
+ sys.base_exec_prefix = sysconfig.get_config_var("prefix")
33
+ else:
34
+ sys.base_prefix = sysconfig._get_sysconfigdata()["prefix"]
35
+ sys.base_exec_prefix = sysconfig._get_sysconfigdata()["prefix"]
36
+
37
+ ###########################################################################
38
+ # os module patches
39
+ ###########################################################################
40
+ @monkeypatch(os)
41
+ def uname():
42
+ return os.uname_result(
43
+ (
44
+ "{os_sysname}",
45
+ "{os_nodename}",
46
+ "{os_release}",
47
+ "{os_version}",
48
+ "{arch}",
49
+ )
50
+ )
51
+
52
+ ###########################################################################
53
+ # platform module patches
54
+ ###########################################################################
55
+ @monkeypatch(platform)
56
+ def uname():
57
+ return platform.uname_result(
58
+ system="{os}",
59
+ node="{os_nodename}",
60
+ release="{release}",
61
+ version="{os_version}",
62
+ machine="{arch}",
63
+ )
64
+
65
+ # -------------------------------------------------------------------------
66
+ {platform_extra}
67
+ ###########################################################################
68
+ # subprocess module patches
69
+ ###########################################################################
70
+ subprocess._can_fork_exec = True
71
+
72
+ ###########################################################################
73
+ # sysconfig module patches
74
+ ###########################################################################
75
+
76
+ @monkeypatch(sysconfig)
77
+ def get_platform():
78
+ return "{platform}-{platform_version}-{machine}"
79
+
80
+ @monkeypatch(sysconfig)
81
+ def get_sysconfigdata_name():
82
+ return "_sysconfigdata__{platform}_{multiarch}"
83
+
84
+ # Ensure module-level values cached at time of import are updated.
85
+ sysconfig._BASE_PREFIX = sys.base_prefix
86
+ sysconfig._BASE_EXEC_PREFIX = sys.base_exec_prefix
87
+
88
+ # Force sysconfig data to be loaded (and cached).
89
+ sysconfig._init_config_vars()
90
+
91
+
92
+ # if XBUILD_ENV=off is in the environment, don't apply any environment shims.
93
+ # This reverts the environment to a native build environment.
94
+ if os.environ.get("XBUILD_ENV", "on").lower() in {{"1", "on"}}:
95
+ patch_cross_env()
xvenv/convert.py ADDED
@@ -0,0 +1,174 @@
1
+ import json
2
+ import pprint
3
+ import sys
4
+ from importlib import import_module
5
+ from importlib import util as importlib_util
6
+ from pathlib import Path
7
+
8
+
9
+ def localized_vars(orig_vars, slice_path):
10
+ """Update (where possible) any references to build-time variables with the best
11
+ guess of the installed location."""
12
+ # The host's sysconfigdata will include references to build-time variables.
13
+ # Update these to refer to the current known install location.
14
+ orig_prefix = orig_vars["prefix"]
15
+ localized_vars = {}
16
+ for key, value in orig_vars.items():
17
+ final = value
18
+ if isinstance(value, str):
19
+ # Replace any reference to the build installation prefix
20
+ final = final.replace(orig_prefix, str(slice_path))
21
+ # Replace any reference to the build-time Framework location
22
+ final = final.replace("-F .", f"-F {slice_path}")
23
+ localized_vars[key] = final
24
+
25
+ return localized_vars
26
+
27
+
28
+ def localize_sysconfigdata(sysconfigdata_path, venv_site_packages):
29
+ """Localize a sysconfigdata python module.
30
+
31
+ :param support_path: The platform config that contains the sysconfigdata module to
32
+ localize.
33
+ :param venv_site_packages: The site packages folder where the localized
34
+ sysconfigdata module should be output.
35
+ """
36
+ # Import the sysconfigdata module
37
+ spec = importlib_util.spec_from_file_location(
38
+ sysconfigdata_path.stem, sysconfigdata_path
39
+ )
40
+ if spec is None:
41
+ msg = f"Unable to load spec for {sysconfigdata_path}"
42
+ raise ValueError(msg)
43
+ if spec.loader is None:
44
+ msg = f"Spec for {sysconfigdata_path} does not define a loader"
45
+ raise ValueError(msg)
46
+ sysconfigdata = importlib_util.module_from_spec(spec)
47
+ spec.loader.exec_module(sysconfigdata)
48
+
49
+ # Write the updated sysconfigdata module into the cross-platform site.
50
+ slice_path = sysconfigdata_path.parent.parent.parent
51
+ with (venv_site_packages / sysconfigdata_path.name).open("w") as f:
52
+ f.write(f"# Generated from {sysconfigdata_path}\n")
53
+ f.write("build_time_vars = ")
54
+ pprint.pprint(
55
+ localized_vars(sysconfigdata.build_time_vars, slice_path),
56
+ stream=f,
57
+ compact=True,
58
+ )
59
+
60
+ return sysconfigdata.build_time_vars
61
+
62
+
63
+ def localize_sysconfig_vars(sysconfig_vars_path, venv_site_packages):
64
+ """Localize a sysconfig_vars.json file.
65
+
66
+ :param support_path: The platform config that contains the sysconfigdata module to
67
+ localize.
68
+ :param venv_site_packages: The site-packages folder where the localized
69
+ sysconfig_vars.json file should be output.
70
+ :return: The localized sysconfig
71
+ """
72
+ with sysconfig_vars_path.open("rb") as f:
73
+ build_time_vars = json.load(f)
74
+
75
+ prefix = sysconfig_vars_path.parent.parent.parent
76
+ sysconfig_vars = localized_vars(build_time_vars, prefix)
77
+
78
+ with (venv_site_packages / sysconfig_vars_path.name).open("w") as f:
79
+ json.dump(sysconfig_vars, f, indent=2)
80
+
81
+ return sysconfig_vars
82
+
83
+
84
+ def convert_venv(venv_path: Path, sysconfig_path: Path):
85
+ """Convert a virtual environment into a cross-platform environment.
86
+
87
+ :param venv_path: The path to the root of the venv.
88
+ :param sysconfig_path: The path to the sysconfig_vars JSON or sysconfigdata Python
89
+ file for the target platform.
90
+ """
91
+ if not venv_path.exists():
92
+ raise ValueError(f"Virtual environment {venv_path} does not exist.")
93
+ if not (venv_path / "bin/python3").exists():
94
+ raise ValueError(f"{venv_path} does not appear to be a virtual environment.")
95
+
96
+ # Update path references in the sysconfigdata to reflect local conditions.
97
+ platlibs = list(venv_path.glob("lib/*/site-packages"))
98
+ if len(platlibs) == 0:
99
+ raise ValueError(f"Couldn't find site packages in {venv_path}")
100
+ elif len(platlibs) > 1:
101
+ raise ValueError(f"Found more than one site packages in {venv_path}")
102
+
103
+ venv_site_packages_path = platlibs[0]
104
+
105
+ if not sysconfig_path.is_file():
106
+ raise ValueError(f"Could not find sysconfig file {sysconfig_path}")
107
+
108
+ match sysconfig_path.suffix:
109
+ case ".json":
110
+ sysconfig_vars_path = sysconfig_path
111
+ _, _, _, abiflags, platform, multiarch = sysconfig_vars_path.stem.split("_")
112
+
113
+ sysconfigdata_path = (
114
+ sysconfig_vars_path.parent
115
+ / f"_sysconfigdata_{abiflags}_{platform}_{multiarch}.py"
116
+ )
117
+ case ".py":
118
+ sysconfigdata_path = sysconfig_path
119
+ _, _, abiflags, platform, multiarch = sysconfigdata_path.stem.split("_")
120
+
121
+ sysconfig_vars_path = (
122
+ sysconfigdata_path.parent
123
+ / f"_sysconfig_vars_{abiflags}_{platform}_{multiarch}.py"
124
+ )
125
+ case _:
126
+ raise ValueError(
127
+ "Don't know how to process sysconfig data "
128
+ f"of type {sysconfig_path.suffix}"
129
+ )
130
+
131
+ # Localize the sysconfig data. sysconfigdata *must* exist; sysconfig_vars
132
+ # will only exist on Python 3.14 or newer.
133
+ sysconfig = localize_sysconfigdata(sysconfigdata_path, venv_site_packages_path)
134
+ if sys.version_info[:2] >= (3, 14):
135
+ localize_sysconfig_vars(sysconfig_vars_path, venv_site_packages_path)
136
+
137
+ if sysconfig["VERSION"] != venv_site_packages_path.parts[-2][6:]:
138
+ raise ValueError(
139
+ f"target venv is Python {venv_site_packages_path.parts[-2][6:]}; "
140
+ f"sysconfig file is for Python {sysconfig['VERSION']}"
141
+ )
142
+
143
+ # Generate the context for the templated cross-target file
144
+ arch, sdk = multiarch.split("-", 1)
145
+ context = {
146
+ "platform": platform,
147
+ "os": platform, # some platforms use different capitalization here
148
+ "multiarch": multiarch,
149
+ "abiflags": abiflags,
150
+ "arch": arch,
151
+ "sdk": sdk,
152
+ }
153
+
154
+ try:
155
+ platform_module = import_module(f"xvenv.platforms.{platform}")
156
+ platform_module.extend_context(context, sysconfig)
157
+ except ImportError:
158
+ raise ValueError(
159
+ f"Don't know how to build a cross-venv file for {platform}"
160
+ ) from None
161
+
162
+ cross_multiarch = f"_cross_{platform}_{multiarch.replace('-', '_')}"
163
+
164
+ # Render the template for the cross-target file.
165
+ template = (Path(__file__).parent / "_cross_target.py.tmpl").read_text()
166
+ rendered = template.format(**context)
167
+ (venv_site_packages_path / f"{cross_multiarch}.py").write_text(rendered)
168
+
169
+ # Write the .pth file that will enable the cross-target modifications
170
+ (venv_site_packages_path / "_cross_venv.pth").write_text(
171
+ f"import {cross_multiarch}\n"
172
+ )
173
+
174
+ return f"{context['os']} {multiarch}"
File without changes
@@ -0,0 +1,79 @@
1
+ def extend_context(context, sysconfig):
2
+ # Convert the API level into a release number
3
+ api_level = int(sysconfig["ANDROID_API_LEVEL"])
4
+ if api_level >= 33:
5
+ release = f"{api_level - 20}"
6
+ elif api_level == 32:
7
+ release = "12L"
8
+ elif api_level == 31:
9
+ release = "12"
10
+ elif api_level > 28:
11
+ release = f"{api_level - 19}"
12
+ elif api_level == 27:
13
+ release = "8.1"
14
+ elif api_level == 26:
15
+ release = "8.0"
16
+ elif api_level == 25:
17
+ release = "7.1"
18
+ elif api_level == 24:
19
+ release = "7.0"
20
+ elif api_level == 23:
21
+ release = "6.0"
22
+ elif api_level == 22:
23
+ release = "5.1"
24
+ elif api_level == 21:
25
+ release = "5.0"
26
+ elif api_level == 20:
27
+ release = "4.4W"
28
+ else:
29
+ raise ValueError("xbuild doesn't support API levels lower than 20")
30
+
31
+ ######################################################################
32
+ context["os"] = "Android"
33
+ context["release"] = release
34
+ context["platform_version"] = api_level
35
+ context["machine"] = {
36
+ "x86_64": "x86_64",
37
+ "i686": "x86",
38
+ "aarch64": "arm64_v8a",
39
+ "armv7l": "armeabi_v7a",
40
+ }[context["arch"]]
41
+
42
+ # The Linux kernel version and release are unlikely to be
43
+ # significant, but return realistic values anyway (from an
44
+ # API level 24 emulator).
45
+ context["os_sysname"] = "Linux"
46
+ context["os_nodename"] = "localhost"
47
+ context["os_release"] = "3.18.91+"
48
+ context["os_version"] = "#1 SMP PREEMPT Tue Jan 9 20:35:43 UTC 2018"
49
+
50
+ context["platform_extra"] = f"""
51
+ @monkeypatch(platform)
52
+ def getandroidapilevel() -> int:
53
+ return {api_level}
54
+
55
+ @monkeypatch(platform)
56
+ def android_ver(
57
+ release="",
58
+ api_level=0,
59
+ manufacturer="",
60
+ model="",
61
+ device="",
62
+ is_emulator=False
63
+ ):
64
+ if release == "":
65
+ release = "{release}"
66
+ if api_level == 0:
67
+ api_level = {api_level}
68
+ if manufacturer == "":
69
+ manufacturer = "Google"
70
+ if model == "":
71
+ model = "sdk_gphone64"
72
+ if device == "":
73
+ device = "emu64"
74
+
75
+ return platform.AndroidVer(
76
+ release, api_level, manufacturer, model, device, True
77
+ )
78
+
79
+ """
@@ -0,0 +1,16 @@
1
+ def extend_context(context, sysconfig):
2
+ emscripten_version = "4.0.12"
3
+ context["release"] = emscripten_version
4
+ context["platform_version"] = emscripten_version
5
+ context["machine"] = context["arch"]
6
+
7
+ context["os_sysname"] = "Emscripten"
8
+ context["os_nodename"] = "emscripten"
9
+ context["os_release"] = emscripten_version
10
+ context["os_version"] = "#1"
11
+
12
+ context["platform_extra"] = f"""
13
+ @monkeypatch(platform)
14
+ def libc_ver() -> int:
15
+ return ("emscripten", "{emscripten_version}")
16
+ """
xvenv/platforms/ios.py ADDED
@@ -0,0 +1,33 @@
1
+ def extend_context(context, sysconfig):
2
+ release = sysconfig["IPHONEOS_DEPLOYMENT_TARGET"]
3
+ is_simulator = context["sdk"] == "iphonesimulator"
4
+
5
+ ######################################################################
6
+ context["os"] = "iOS"
7
+ context["release"] = release
8
+ context["platform_version"] = release
9
+ context["machine"] = context["multiarch"]
10
+
11
+ # The Darwin kernel version and release are unlikely to be
12
+ # significant, but return realistic values anyway (from an
13
+ # iPhone simulator).
14
+ context["os_sysname"] = "Darwin"
15
+ context["os_nodename"] = "buildmachine.local"
16
+ context["os_release"] = "24.4.0"
17
+ context["os_version"] = (
18
+ "Darwin Kernel Version 24.4.0: Fri Apr 11 18:33:47 PDT 2025; "
19
+ "root:xnu-11417.101.15~117/RELEASE_ARM64_T6000"
20
+ )
21
+
22
+ context["platform_extra"] = f"""
23
+ @monkeypatch(platform)
24
+ def ios_ver(system="", release="", model="", is_simulator=False):
25
+ if system == "":
26
+ system = "iOS"
27
+ if release == "":
28
+ release = "{release}"
29
+ if model == "":
30
+ model = "{context["sdk"]}"
31
+
32
+ return platform.IOSVersionInfo(system, release, model, {is_simulator})
33
+ """