Skip to content
Merged
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
1 change: 1 addition & 0 deletions changes/101.feature.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions docs/en/reference/environment-variables.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,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. Uses the same syntax as the platform's `PATH` variable.

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
```

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.

`XBUILD_PATH` only applies to builds performed by `xbuild`. It has no effect on a cross-platform environment that is activated and used directly.
2 changes: 2 additions & 0 deletions docs/en/topics/how-it-works.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
1 change: 1 addition & 0 deletions docs/spelling_wordlist
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ monkeypatching
natively
NDK
pre
prepended
PyPI
README
SDK
Expand Down
16 changes: 15 additions & 1 deletion src/xvenv/convert.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import json
import os
import pprint
import re
import sys
Expand Down Expand Up @@ -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()`
Expand All @@ -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
Expand Down
50 changes: 46 additions & 4 deletions tests/_xvenv/test_convert.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import io
import json
import os
import sys
from pathlib import Path
from unittest.mock import Mock
Expand Down Expand Up @@ -389,17 +390,58 @@ def test_config_missing_sysconfigdata_file(tmp_path):
)


def test_prepare_env_dispatches_to_platform_module(tmp_path, mock_config):
"""`prepare_env()` delegates to the platform module."""
@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)

fake_platform_module = Mock()
fake_platform_module.prepare_env.return_value = {"CC": "fake-clang"}
fake_platform_module.prepare_env.return_value = {
"CC": "fake-clang",
"PATH": os.pathsep.join(orig_path),
}

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"}
assert result == {
"CC": "fake-clang",
"PATH": os.pathsep.join(final_path),
}


def _fake_venv(venv_path, version):
Expand Down
Loading