From 4b7184a324256692d1e327572e7deee77680c22e Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 6 Oct 2026 15:58:39 +0800 Subject: [PATCH 1/5] Prepend XBUILD_PATH to PATH when preparing a cross build environment. --- src/xvenv/convert.py | 16 +++++- tests/_xvenv/test_convert.py | 101 ++++++++++++++++++++++++++++++++++- 2 files changed, 115 insertions(+), 2 deletions(-) diff --git a/src/xvenv/convert.py b/src/xvenv/convert.py index ae762e6..b81a7a1 100644 --- a/src/xvenv/convert.py +++ b/src/xvenv/convert.py @@ -1,6 +1,7 @@ from __future__ import annotations import json +import os import pprint import re import sys @@ -361,6 +362,11 @@ def create(self, venv_path: Path, with_pip: bool = True): def prepare_env(self) -> dict[str, str]: """Prepare the environment variables needed to build for this cross environment. + If the `XBUILD_PATH` environment variable is set (and non-empty), it is + prepended to `PATH`. If the platform module provides a `PATH` (e.g., iOS, + which replaces `PATH` entirely), `XBUILD_PATH` is prepended to that value; + otherwise, it is prepended to the inherited `PATH`. + :returns: A dict of environment variables to merge into `os.environ` for the duration of the build. :raises ValueError: if the platform module's own `prepare_env()` @@ -369,7 +375,15 @@ def prepare_env(self) -> dict[str, str]: :raises NotImplementedError: for platforms that don't support environment preparation yet (currently: emscripten). """ - return self.platform_module.prepare_env(self) + env = dict(self.platform_module.prepare_env(self)) + + if xbuild_path := os.environ.get("XBUILD_PATH"): + base_path = env.get("PATH", os.environ.get("PATH", "")) + env["PATH"] = os.pathsep.join( + part for part in [xbuild_path, base_path] if part + ) + + return env def packages_path(self, work_path: Path) -> Path: """Return the location, relative to the provided path, where the diff --git a/tests/_xvenv/test_convert.py b/tests/_xvenv/test_convert.py index 17c3c1f..5b48f9c 100644 --- a/tests/_xvenv/test_convert.py +++ b/tests/_xvenv/test_convert.py @@ -1,5 +1,6 @@ import io import json +import os import sys from pathlib import Path from unittest.mock import Mock @@ -389,8 +390,9 @@ def test_config_missing_sysconfigdata_file(tmp_path): ) -def test_prepare_env_dispatches_to_platform_module(tmp_path, mock_config): +def test_prepare_env_dispatches_to_platform_module(tmp_path, mock_config, monkeypatch): """`prepare_env()` delegates to the platform module.""" + monkeypatch.delenv("XBUILD_PATH", raising=False) fake_platform_module = Mock() fake_platform_module.prepare_env.return_value = {"CC": "fake-clang"} @@ -402,6 +404,103 @@ def test_prepare_env_dispatches_to_platform_module(tmp_path, mock_config): assert result == {"CC": "fake-clang"} +def _fake_platform(env): + """A fake platform module whose `prepare_env()` returns `env`.""" + fake_platform_module = Mock() + fake_platform_module.prepare_env.return_value = env + return fake_platform_module + + +def test_prepare_env_platform_path_without_xbuild_path(mock_config, monkeypatch): + """With no XBUILD_PATH, a PATH provided by the platform is used unchanged.""" + monkeypatch.delenv("XBUILD_PATH", raising=False) + mock_config.platform_module = _fake_platform( + {"PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} + ) + + result = mock_config.prepare_env() + + assert result == {"PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} + + +def test_prepare_env_empty_xbuild_path_ignored(mock_config, monkeypatch): + """An empty XBUILD_PATH is treated as unset; no PATH is added.""" + monkeypatch.setenv("XBUILD_PATH", "") + mock_config.platform_module = _fake_platform({"CC": "fake-clang"}) + + result = mock_config.prepare_env() + + assert result == {"CC": "fake-clang"} + + +def test_prepare_env_xbuild_path_prepended_to_platform_path(mock_config, monkeypatch): + """XBUILD_PATH is prepended to a PATH provided by the platform (e.g., iOS), + rather than to the inherited PATH.""" + monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") + monkeypatch.setenv("PATH", "/inherited/bin") + mock_config.platform_module = _fake_platform( + {"CC": "fake-clang", "PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} + ) + + result = mock_config.prepare_env() + + assert result == { + "CC": "fake-clang", + "PATH": os.pathsep.join(["/opt/tools/bin", "/venv/bin", "/usr/bin"]), + } + + +def test_prepare_env_xbuild_path_prepended_to_inherited_path(mock_config, monkeypatch): + """If the platform doesn't provide PATH (e.g., Android), XBUILD_PATH is + prepended to the inherited PATH.""" + monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") + monkeypatch.setenv("PATH", os.pathsep.join(["/inherited/bin", "/usr/bin"])) + mock_config.platform_module = _fake_platform({"CC": "fake-clang"}) + + result = mock_config.prepare_env() + + assert result == { + "CC": "fake-clang", + "PATH": os.pathsep.join(["/opt/tools/bin", "/inherited/bin", "/usr/bin"]), + } + + +def test_prepare_env_xbuild_path_multiple_entries(mock_config, monkeypatch): + """XBUILD_PATH may contain multiple entries; they are preserved in order.""" + xbuild_path = os.pathsep.join(["/opt/a/bin", "/opt/b/bin"]) + monkeypatch.setenv("XBUILD_PATH", xbuild_path) + mock_config.platform_module = _fake_platform({"PATH": "/usr/bin"}) + + result = mock_config.prepare_env() + + assert result == { + "PATH": os.pathsep.join(["/opt/a/bin", "/opt/b/bin", "/usr/bin"]), + } + + +def test_prepare_env_xbuild_path_with_no_base_path(mock_config, monkeypatch): + """If there's no PATH at all, PATH is just XBUILD_PATH, with no empty + trailing component.""" + monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") + monkeypatch.delenv("PATH", raising=False) + mock_config.platform_module = _fake_platform({}) + + result = mock_config.prepare_env() + + assert result == {"PATH": "/opt/tools/bin"} + + +def test_prepare_env_does_not_mutate_platform_result(mock_config, monkeypatch): + """The dict returned by the platform module isn't modified in place.""" + monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") + platform_env = {"PATH": "/usr/bin"} + mock_config.platform_module = _fake_platform(platform_env) + + mock_config.prepare_env() + + assert platform_env == {"PATH": "/usr/bin"} + + def _fake_venv(venv_path, version): """Create the minimal on-disk layout that `CrossVenvConfig.convert()` accepts as a virtual environment, returning the site-packages path.""" From d441d6214bc5ab8db80dd9cb9d275bb5c1b0f45f Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 6 Oct 2026 16:02:19 +0800 Subject: [PATCH 2/5] Document XBUILD_PATH, and add changenote. --- changes/101.feature.md | 1 + docs/en/reference/environment-variables.md | 14 ++++++++++++++ docs/en/topics/how-it-works.md | 2 ++ docs/spelling_wordlist | 1 + 4 files changed, 18 insertions(+) create mode 100644 changes/101.feature.md diff --git a/changes/101.feature.md b/changes/101.feature.md new file mode 100644 index 0000000..b4dc2bd --- /dev/null +++ b/changes/101.feature.md @@ -0,0 +1 @@ +If the `XBUILD_PATH` environment variable is set, its contents are now prepended to the `PATH` used by `xbuild` when building for a target platform. diff --git a/docs/en/reference/environment-variables.md b/docs/en/reference/environment-variables.md index 30fadc3..a26b951 100644 --- a/docs/en/reference/environment-variables.md +++ b/docs/en/reference/environment-variables.md @@ -4,6 +4,20 @@ The directory used to cache downloaded target-platform Python builds when using `--platform`. Overridden by the `--cache` command-line option, which takes precedence. If neither is set, defaults to a platform-appropriate user cache directory (via [`platformdirs.user_cache_dir("xbuild")`](https://pypi.org/project/platformdirs/)). +## `XBUILD_PATH` + +Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Use `os.pathsep` (`:` on macOS and Linux) to separate multiple directories. + +This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g., `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: + +```console +(venv) $ XBUILD_PATH=/opt/homebrew/opt/cmake/bin xbuild --platform ios --arch arm64-iphonesimulator +``` + +On other platforms, `XBUILD_PATH` is prepended to the inherited `PATH`. If `XBUILD_PATH` is unset or empty, `PATH` is not modified. + +`XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly. + ## `XBUILD_ENV` Controls whether an active cross-platform virtual environment's patches (to `sys`, `os`, `platform`, and `sysconfig` - see [How it works](../topics/how-it-works.md)) are applied. Set to `off` to temporarily make an active cross-platform environment behave like a normal build-platform environment: diff --git a/docs/en/topics/how-it-works.md b/docs/en/topics/how-it-works.md index 63b83ec..f994223 100644 --- a/docs/en/topics/how-it-works.md +++ b/docs/en/topics/how-it-works.md @@ -38,6 +38,8 @@ In addition to converting a virtual environment into a cross-platform environmen - **Android**: `CC`, `AR`, `AS`, `CXX`, `LD`, `NM`, `RANLIB`, `READELF`, `STRIP`, `CFLAGS`, `LDFLAGS`, `CXXFLAGS`, `CPU_COUNT`, and (if available) `PKG_CONFIG`/`PKG_CONFIG_LIBDIR` are set by running the target Android Python build's own bundled `android.py env` command (which installs the exact required NDK version under `$ANDROID_HOME/ndk/` if it isn't already present). This requires `ANDROID_HOME` to already be set - see [Platform setup: Android](../how-to/platform-setup/android.md). - **iOS**: `PATH` is replaced with the cross-platform environment's own `bin/` directory, followed by the target-platform `Python.xcframework` slice's `bin/` directory (containing the `clang`/`ar`/`strip` shims for the target architecture), followed by a fixed, minimal set of system directories, ensuring no build-machine-native tools leak into the build. +On all platforms, if the [`XBUILD_PATH`](../reference/environment-variables.md#xbuild_path) environment variable is set, its contents are prepended to `PATH`. This allows specific build-machine tools to be made available to the build, even on platforms (such as iOS) where `PATH` is otherwise replaced. + Build-platform tools (e.g. Android's `android.py`, or the iOS testbed driver) are always run with `XBUILD_ENV=off`, so that they behave correctly even when `xbuild` or `xpython` is itself running inside a cross-platform environment. This preparation is `xbuild`-specific; it does not apply to a cross-platform environment created by `xvenv` and used directly outside of `xbuild`. diff --git a/docs/spelling_wordlist b/docs/spelling_wordlist index 9c526f4..db1195b 100644 --- a/docs/spelling_wordlist +++ b/docs/spelling_wordlist @@ -13,6 +13,7 @@ monkeypatching natively NDK pre +prepended PyPI README SDK From 755c92f9cc4ce8726ff8881aa0fb68c7e98a85ce Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 6 Oct 2026 16:07:41 +0800 Subject: [PATCH 3/5] Order environment variable reference alphabetically. --- docs/en/reference/environment-variables.md | 28 +++++++++++----------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/en/reference/environment-variables.md b/docs/en/reference/environment-variables.md index a26b951..02e27c8 100644 --- a/docs/en/reference/environment-variables.md +++ b/docs/en/reference/environment-variables.md @@ -4,20 +4,6 @@ The directory used to cache downloaded target-platform Python builds when using `--platform`. Overridden by the `--cache` command-line option, which takes precedence. If neither is set, defaults to a platform-appropriate user cache directory (via [`platformdirs.user_cache_dir("xbuild")`](https://pypi.org/project/platformdirs/)). -## `XBUILD_PATH` - -Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Use `os.pathsep` (`:` on macOS and Linux) to separate multiple directories. - -This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g., `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: - -```console -(venv) $ XBUILD_PATH=/opt/homebrew/opt/cmake/bin xbuild --platform ios --arch arm64-iphonesimulator -``` - -On other platforms, `XBUILD_PATH` is prepended to the inherited `PATH`. If `XBUILD_PATH` is unset or empty, `PATH` is not modified. - -`XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly. - ## `XBUILD_ENV` Controls whether an active cross-platform virtual environment's patches (to `sys`, `os`, `platform`, and `sysconfig` - see [How it works](../topics/how-it-works.md)) are applied. Set to `off` to temporarily make an active cross-platform environment behave like a normal build-platform environment: @@ -31,3 +17,17 @@ darwin ``` Clearing the variable, or setting it (case-insensitively) to `1` or `on`, leaves the cross-platform patches active. + +## `XBUILD_PATH` + +Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Use `os.pathsep` (`:` on macOS and Linux) to separate multiple directories. + +This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g., `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: + +```console +(venv) $ XBUILD_PATH=/opt/homebrew/opt/cmake/bin xbuild --platform ios --arch arm64-iphonesimulator +``` + +On other platforms, `XBUILD_PATH` is prepended to the inherited `PATH`. If `XBUILD_PATH` is unset or empty, `PATH` is not modified. + +`XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly. From 2d3bf0c2a51a895850e4703763e3732c623fa158 Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Tue, 6 Oct 2026 16:12:47 +0800 Subject: [PATCH 4/5] Warn about XBUILD_PATH shadowing target-platform tools. --- docs/en/reference/environment-variables.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/environment-variables.md b/docs/en/reference/environment-variables.md index 02e27c8..d4ad993 100644 --- a/docs/en/reference/environment-variables.md +++ b/docs/en/reference/environment-variables.md @@ -20,14 +20,16 @@ Clearing the variable, or setting it (case-insensitively) to `1` or `on`, leaves ## `XBUILD_PATH` -Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Use `os.pathsep` (`:` on macOS and Linux) to separate multiple directories. +Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Separate multiple directories with `:`. -This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g., `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: +This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g. `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: ```console (venv) $ XBUILD_PATH=/opt/homebrew/opt/cmake/bin xbuild --platform ios --arch arm64-iphonesimulator ``` +Directories in `XBUILD_PATH` take precedence over the tools `xbuild` provides for the target platform. Point `XBUILD_PATH` at directories that contain only the specific tools you need, rather than at a general directory like `/opt/homebrew/bin`, which could shadow the target-platform compiler or Python interpreter. + On other platforms, `XBUILD_PATH` is prepended to the inherited `PATH`. If `XBUILD_PATH` is unset or empty, `PATH` is not modified. `XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly. From 40a360bef37ce1a42f977e76337a9a0613448f7c Mon Sep 17 00:00:00 2001 From: Russell Keith-Magee Date: Wed, 7 Oct 2026 07:25:39 +0800 Subject: [PATCH 5/5] Clean up tests and docs. --- docs/en/reference/environment-variables.md | 6 +- tests/_xvenv/test_convert.py | 139 ++++++--------------- 2 files changed, 43 insertions(+), 102 deletions(-) diff --git a/docs/en/reference/environment-variables.md b/docs/en/reference/environment-variables.md index d4ad993..8961bc7 100644 --- a/docs/en/reference/environment-variables.md +++ b/docs/en/reference/environment-variables.md @@ -20,9 +20,9 @@ Clearing the variable, or setting it (case-insensitively) to `1` or `on`, leaves ## `XBUILD_PATH` -Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Separate multiple directories with `:`. +Additional directories to *prepend* to `PATH` when `xbuild` prepares the build environment for the target platform. Uses the same syntax as the platform's `PATH` variable. -This is most useful when building for iOS. `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g. `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: +This variable is honored on all platforms, but it is most useful when building for iOS. When building for iOS, `xbuild` replaces `PATH` with a minimal, clean value so that no build-machine tools leak into the build, but some builds still need specific build-machine tools (e.g. `cmake` or `ninja`). Directories in `XBUILD_PATH` are added ahead of that clean `PATH`: ```console (venv) $ XBUILD_PATH=/opt/homebrew/opt/cmake/bin xbuild --platform ios --arch arm64-iphonesimulator @@ -30,6 +30,4 @@ This is most useful when building for iOS. `xbuild` replaces `PATH` with a minim Directories in `XBUILD_PATH` take precedence over the tools `xbuild` provides for the target platform. Point `XBUILD_PATH` at directories that contain only the specific tools you need, rather than at a general directory like `/opt/homebrew/bin`, which could shadow the target-platform compiler or Python interpreter. -On other platforms, `XBUILD_PATH` is prepended to the inherited `PATH`. If `XBUILD_PATH` is unset or empty, `PATH` is not modified. - `XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly. diff --git a/tests/_xvenv/test_convert.py b/tests/_xvenv/test_convert.py index 5b48f9c..454aecd 100644 --- a/tests/_xvenv/test_convert.py +++ b/tests/_xvenv/test_convert.py @@ -390,117 +390,60 @@ def test_config_missing_sysconfigdata_file(tmp_path): ) -def test_prepare_env_dispatches_to_platform_module(tmp_path, mock_config, monkeypatch): - """`prepare_env()` delegates to the platform module.""" - monkeypatch.delenv("XBUILD_PATH", raising=False) - fake_platform_module = Mock() - fake_platform_module.prepare_env.return_value = {"CC": "fake-clang"} - - mock_config.platform_module = fake_platform_module - - result = mock_config.prepare_env() - - fake_platform_module.prepare_env.assert_called_once_with(mock_config) - assert result == {"CC": "fake-clang"} - +@pytest.mark.parametrize( + ("orig_path", "xbuild_path", "final_path"), + [ + # XBUILD_PATH isn't defined + ( + ["/venv/bin", "/usr/bin"], + None, + ["/venv/bin", "/usr/bin"], + ), + # XBUILD_PATH defined, but empty + ( + ["/venv/bin", "/usr/bin"], + "", + ["/venv/bin", "/usr/bin"], + ), + # XBUILD_PATH with actual values + ( + ["/venv/bin", "/usr/bin"], + "/local/bin", + ["/local/bin", "/venv/bin", "/usr/bin"], + ), + ( + ["/venv/bin", "/usr/bin"], + ["/other/bin", "/local/bin"], + ["/other/bin", "/local/bin", "/venv/bin", "/usr/bin"], + ), + ], +) +def test_prepare_env(mock_config, orig_path, xbuild_path, final_path, monkeypatch): + """`prepare_env()` delegates to the platform module and handles `XBUILD_PATH`.""" + if xbuild_path is None: + monkeypatch.delenv("XBUILD_PATH", raising=False) + else: + if isinstance(xbuild_path, list): + xbuild_path = os.pathsep.join(xbuild_path) + monkeypatch.setenv("XBUILD_PATH", xbuild_path) -def _fake_platform(env): - """A fake platform module whose `prepare_env()` returns `env`.""" fake_platform_module = Mock() - fake_platform_module.prepare_env.return_value = env - return fake_platform_module - - -def test_prepare_env_platform_path_without_xbuild_path(mock_config, monkeypatch): - """With no XBUILD_PATH, a PATH provided by the platform is used unchanged.""" - monkeypatch.delenv("XBUILD_PATH", raising=False) - mock_config.platform_module = _fake_platform( - {"PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} - ) - - result = mock_config.prepare_env() - - assert result == {"PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} - - -def test_prepare_env_empty_xbuild_path_ignored(mock_config, monkeypatch): - """An empty XBUILD_PATH is treated as unset; no PATH is added.""" - monkeypatch.setenv("XBUILD_PATH", "") - mock_config.platform_module = _fake_platform({"CC": "fake-clang"}) - - result = mock_config.prepare_env() - - assert result == {"CC": "fake-clang"} - - -def test_prepare_env_xbuild_path_prepended_to_platform_path(mock_config, monkeypatch): - """XBUILD_PATH is prepended to a PATH provided by the platform (e.g., iOS), - rather than to the inherited PATH.""" - monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") - monkeypatch.setenv("PATH", "/inherited/bin") - mock_config.platform_module = _fake_platform( - {"CC": "fake-clang", "PATH": os.pathsep.join(["/venv/bin", "/usr/bin"])} - ) - - result = mock_config.prepare_env() - - assert result == { + fake_platform_module.prepare_env.return_value = { "CC": "fake-clang", - "PATH": os.pathsep.join(["/opt/tools/bin", "/venv/bin", "/usr/bin"]), + "PATH": os.pathsep.join(orig_path), } - -def test_prepare_env_xbuild_path_prepended_to_inherited_path(mock_config, monkeypatch): - """If the platform doesn't provide PATH (e.g., Android), XBUILD_PATH is - prepended to the inherited PATH.""" - monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") - monkeypatch.setenv("PATH", os.pathsep.join(["/inherited/bin", "/usr/bin"])) - mock_config.platform_module = _fake_platform({"CC": "fake-clang"}) + mock_config.platform_module = fake_platform_module result = mock_config.prepare_env() + fake_platform_module.prepare_env.assert_called_once_with(mock_config) assert result == { "CC": "fake-clang", - "PATH": os.pathsep.join(["/opt/tools/bin", "/inherited/bin", "/usr/bin"]), + "PATH": os.pathsep.join(final_path), } -def test_prepare_env_xbuild_path_multiple_entries(mock_config, monkeypatch): - """XBUILD_PATH may contain multiple entries; they are preserved in order.""" - xbuild_path = os.pathsep.join(["/opt/a/bin", "/opt/b/bin"]) - monkeypatch.setenv("XBUILD_PATH", xbuild_path) - mock_config.platform_module = _fake_platform({"PATH": "/usr/bin"}) - - result = mock_config.prepare_env() - - assert result == { - "PATH": os.pathsep.join(["/opt/a/bin", "/opt/b/bin", "/usr/bin"]), - } - - -def test_prepare_env_xbuild_path_with_no_base_path(mock_config, monkeypatch): - """If there's no PATH at all, PATH is just XBUILD_PATH, with no empty - trailing component.""" - monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") - monkeypatch.delenv("PATH", raising=False) - mock_config.platform_module = _fake_platform({}) - - result = mock_config.prepare_env() - - assert result == {"PATH": "/opt/tools/bin"} - - -def test_prepare_env_does_not_mutate_platform_result(mock_config, monkeypatch): - """The dict returned by the platform module isn't modified in place.""" - monkeypatch.setenv("XBUILD_PATH", "/opt/tools/bin") - platform_env = {"PATH": "/usr/bin"} - mock_config.platform_module = _fake_platform(platform_env) - - mock_config.prepare_env() - - assert platform_env == {"PATH": "/usr/bin"} - - def _fake_venv(venv_path, version): """Create the minimal on-disk layout that `CrossVenvConfig.convert()` accepts as a virtual environment, returning the site-packages path."""