feat: support passenv (#914)
* feat: support passenv * refactor: environment-pass and spaces * docs: document environment pass * Add unit test for passing through awkward env variable values * Remove unused as_shell_commands method from environment * Fix passthrough edge case by avoiding bash for passthrough assignments * Fix mypy error in test * Fix unit test * Apply suggestions from code review Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
co-authored by
Joe Rickerby
parent
1176024473
commit
f45de3f24e
@@ -112,6 +112,7 @@ Options
|
|||||||
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
|
| | [`CIBW_PRERELEASE_PYTHONS`](https://cibuildwheel.readthedocs.io/en/stable/options/#prerelease-pythons) | Enable building with pre-release versions of Python if available |
|
||||||
| **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
|
| **Build customization** | [`CIBW_BUILD_FRONTEND`](https://cibuildwheel.readthedocs.io/en/stable/options/#build-frontend) | Set the tool to use to build, either "pip" (default for now) or "build" |
|
||||||
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
| | [`CIBW_ENVIRONMENT`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment) | Set environment variables needed during the build |
|
||||||
|
| | [`CIBW_ENVIRONMENT_PASS_LINUX`](https://cibuildwheel.readthedocs.io/en/stable/options/#environment-pass) | Set environment variables on the host to pass-through to the container during the build. |
|
||||||
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
|
| | [`CIBW_BEFORE_ALL`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-all) | Execute a shell command on the build system before any wheels are built. |
|
||||||
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
| | [`CIBW_BEFORE_BUILD`](https://cibuildwheel.readthedocs.io/en/stable/options/#before-build) | Execute a shell command preparing each wheel's build |
|
||||||
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel |
|
| | [`CIBW_REPAIR_WHEEL_COMMAND`](https://cibuildwheel.readthedocs.io/en/stable/options/#repair-wheel-command) | Execute a shell command to repair each (non-pure Python) built wheel |
|
||||||
|
|||||||
+45
-10
@@ -1,8 +1,10 @@
|
|||||||
import dataclasses
|
import dataclasses
|
||||||
from typing import Dict, List, Mapping, Optional
|
from typing import Any, Dict, List, Mapping, Optional, Sequence
|
||||||
|
|
||||||
import bashlex
|
import bashlex
|
||||||
|
|
||||||
|
from cibuildwheel.typing import Protocol
|
||||||
|
|
||||||
from . import bashlex_eval
|
from . import bashlex_eval
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +41,41 @@ def split_env_items(env_string: str) -> List[str]:
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
class EnvironmentAssignment:
|
class EnvironmentAssignment(Protocol):
|
||||||
|
name: str
|
||||||
|
|
||||||
|
def evaluated_value(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
environment: Dict[str, str],
|
||||||
|
executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Returns the value of this assignment, as evaluated in the environment"""
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentAssignmentRaw:
|
||||||
|
"""
|
||||||
|
An environment variable - a simple name/value pair
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, value: str):
|
||||||
|
self.name = name
|
||||||
|
self.value = value
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"{self.name}: {self.value}"
|
||||||
|
|
||||||
|
def evaluated_value(self, **kwargs: Any) -> str:
|
||||||
|
return self.value
|
||||||
|
|
||||||
|
|
||||||
|
class EnvironmentAssignmentBash:
|
||||||
|
"""
|
||||||
|
An environment variable, in bash syntax. The value can use bash constructs
|
||||||
|
like "$OTHER_VAR" and "$(command arg1 arg2)".
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, assignment: str):
|
def __init__(self, assignment: str):
|
||||||
name, equals, value = assignment.partition("=")
|
name, equals, value = assignment.partition("=")
|
||||||
if not equals:
|
if not equals:
|
||||||
@@ -52,17 +88,13 @@ class EnvironmentAssignment:
|
|||||||
environment: Dict[str, str],
|
environment: Dict[str, str],
|
||||||
executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
|
executor: Optional[bashlex_eval.EnvironmentExecutor] = None,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""Returns the value of this assignment, as evaluated in the environment"""
|
|
||||||
return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)
|
return bashlex_eval.evaluate(self.value, environment=environment, executor=executor)
|
||||||
|
|
||||||
def as_shell_assignment(self) -> str:
|
|
||||||
return f"export {self.name}={self.value}"
|
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.name}={self.value}"
|
return f"{self.name}={self.value}"
|
||||||
|
|
||||||
def __eq__(self, other: object) -> bool:
|
def __eq__(self, other: object) -> bool:
|
||||||
if isinstance(other, EnvironmentAssignment):
|
if isinstance(other, EnvironmentAssignmentBash):
|
||||||
return self.name == other.name and self.value == other.value
|
return self.name == other.name and self.value == other.value
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -71,6 +103,9 @@ class EnvironmentAssignment:
|
|||||||
class ParsedEnvironment:
|
class ParsedEnvironment:
|
||||||
assignments: List[EnvironmentAssignment]
|
assignments: List[EnvironmentAssignment]
|
||||||
|
|
||||||
|
def __init__(self, assignments: Sequence[EnvironmentAssignment]) -> None:
|
||||||
|
self.assignments = list(assignments)
|
||||||
|
|
||||||
def as_dictionary(
|
def as_dictionary(
|
||||||
self,
|
self,
|
||||||
prev_environment: Mapping[str, str],
|
prev_environment: Mapping[str, str],
|
||||||
@@ -84,8 +119,8 @@ class ParsedEnvironment:
|
|||||||
|
|
||||||
return environment
|
return environment
|
||||||
|
|
||||||
def as_shell_commands(self) -> List[str]:
|
def add(self, name: str, value: str) -> None:
|
||||||
return [a.as_shell_assignment() for a in self.assignments]
|
self.assignments.append(EnvironmentAssignmentRaw(name=name, value=value))
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self) -> str:
|
||||||
return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"
|
return f"{self.__class__.__name__}({[repr(a) for a in self.assignments]!r})"
|
||||||
@@ -93,5 +128,5 @@ class ParsedEnvironment:
|
|||||||
|
|
||||||
def parse_environment(env_string: str) -> ParsedEnvironment:
|
def parse_environment(env_string: str) -> ParsedEnvironment:
|
||||||
env_items = split_env_items(env_string)
|
env_items = split_env_items(env_string)
|
||||||
assignments = [EnvironmentAssignment(item) for item in env_items]
|
assignments = [EnvironmentAssignmentBash(item) for item in env_items]
|
||||||
return ParsedEnvironment(assignments=assignments)
|
return ParsedEnvironment(assignments=assignments)
|
||||||
|
|||||||
@@ -408,6 +408,7 @@ class Options:
|
|||||||
environment_config = self.reader.get(
|
environment_config = self.reader.get(
|
||||||
"environment", table={"item": '{k}="{v}"', "sep": " "}
|
"environment", table={"item": '{k}="{v}"', "sep": " "}
|
||||||
)
|
)
|
||||||
|
environment_pass = self.reader.get("environment-pass", sep=" ").split()
|
||||||
before_build = self.reader.get("before-build", sep=" && ")
|
before_build = self.reader.get("before-build", sep=" && ")
|
||||||
repair_command = self.reader.get("repair-wheel-command", sep=" && ")
|
repair_command = self.reader.get("repair-wheel-command", sep=" && ")
|
||||||
|
|
||||||
@@ -438,6 +439,14 @@ class Options:
|
|||||||
traceback.print_exc(None, sys.stderr)
|
traceback.print_exc(None, sys.stderr)
|
||||||
sys.exit(2)
|
sys.exit(2)
|
||||||
|
|
||||||
|
# Pass through environment variables
|
||||||
|
if self.platform == "linux":
|
||||||
|
for env_var_name in environment_pass:
|
||||||
|
try:
|
||||||
|
environment.add(env_var_name, os.environ[env_var_name])
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
|
||||||
if dependency_versions == "pinned":
|
if dependency_versions == "pinned":
|
||||||
dependency_constraints: Optional[
|
dependency_constraints: Optional[
|
||||||
DependencyConstraints
|
DependencyConstraints
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ archs = ["auto"]
|
|||||||
build-frontend = "pip"
|
build-frontend = "pip"
|
||||||
dependency-versions = "pinned"
|
dependency-versions = "pinned"
|
||||||
environment = {}
|
environment = {}
|
||||||
|
environment-pass = []
|
||||||
build-verbosity = ""
|
build-verbosity = ""
|
||||||
|
|
||||||
before-all = ""
|
before-all = ""
|
||||||
|
|||||||
@@ -605,6 +605,41 @@ Platform-specific environment variables are also available:<br/>
|
|||||||
!!! note
|
!!! note
|
||||||
cibuildwheel always defines the environment variable `CIBUILDWHEEL=1`. This can be useful for [building wheels with optional extensions](faq.md#building-packages-with-optional-c-extensions).
|
cibuildwheel always defines the environment variable `CIBUILDWHEEL=1`. This can be useful for [building wheels with optional extensions](faq.md#building-packages-with-optional-c-extensions).
|
||||||
|
|
||||||
|
### `CIBW_ENVIRONMENT_PASS_LINUX` {: #environment-pass}
|
||||||
|
> Set environment variables on the host to pass-through to the container during the build.
|
||||||
|
|
||||||
|
A list of environment variables to pass into the linux container during the build. It has no affect on the other platforms, which can already access all environment variables directly.
|
||||||
|
|
||||||
|
To specify more than one environment variable, separate the variable names by spaces.
|
||||||
|
|
||||||
|
#### Examples
|
||||||
|
|
||||||
|
!!! tab examples "Environment passthrough"
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# Export a variable
|
||||||
|
CIBW_ENVIRONMENT_PASS_LINUX: CFLAGS
|
||||||
|
|
||||||
|
# Set two flags variables
|
||||||
|
CIBW_ENVIRONMENT_PASS_LINUX: BUILD_TIME SAMPLE_TEXT
|
||||||
|
```
|
||||||
|
|
||||||
|
Separate multiple values with a space.
|
||||||
|
|
||||||
|
!!! tab examples "pyproject.toml"
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[tool.cibuildwheel.linux]
|
||||||
|
|
||||||
|
# Export a variable
|
||||||
|
environment-pass = ["CFLAGS"]
|
||||||
|
|
||||||
|
# Set two flags variables
|
||||||
|
environment-pass = ["BUILD_TIME", "SAMPLE_TEXT"]
|
||||||
|
```
|
||||||
|
|
||||||
|
In configuration mode, you can use a [TOML][] list instead of a raw string as shown above.
|
||||||
|
|
||||||
### `CIBW_BEFORE_ALL` {: #before-all}
|
### `CIBW_BEFORE_ALL` {: #before-all}
|
||||||
> Execute a shell command on the build system before any wheels are built.
|
> Execute a shell command on the build system before any wheels are built.
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pathlib import Path, PurePath
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel.docker_container import DockerContainer
|
from cibuildwheel.docker_container import DockerContainer
|
||||||
from cibuildwheel.environment import EnvironmentAssignment
|
from cibuildwheel.environment import EnvironmentAssignmentBash
|
||||||
|
|
||||||
# for these tests we use manylinux2014 images, because they're available on
|
# for these tests we use manylinux2014 images, because they're available on
|
||||||
# multi architectures and include python3.8
|
# multi architectures and include python3.8
|
||||||
@@ -198,5 +198,5 @@ def test_dir_operations(tmp_path: Path):
|
|||||||
@pytest.mark.docker
|
@pytest.mark.docker
|
||||||
def test_environment_executor():
|
def test_environment_executor():
|
||||||
with DockerContainer(docker_image=DEFAULT_IMAGE) as container:
|
with DockerContainer(docker_image=DEFAULT_IMAGE) as container:
|
||||||
assignment = EnvironmentAssignment("TEST=$(echo 42)")
|
assignment = EnvironmentAssignmentBash("TEST=$(echo 42)")
|
||||||
assert assignment.evaluated_value({}, container.environment_executor) == "42"
|
assert assignment.evaluated_value({}, container.environment_executor) == "42"
|
||||||
|
|||||||
@@ -7,30 +7,24 @@ def test_basic_parsing():
|
|||||||
environment_recipe = parse_environment("VAR=1 VBR=2")
|
environment_recipe = parse_environment("VAR=1 VBR=2")
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {"VAR": "1", "VBR": "2"}
|
assert environment_dict == {"VAR": "1", "VBR": "2"}
|
||||||
assert environment_cmds == ["export VAR=1", "export VBR=2"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_quotes():
|
def test_quotes():
|
||||||
environment_recipe = parse_environment("A=1 VAR=\"1 NOT_A_VAR=2\" VBR='vbr'")
|
environment_recipe = parse_environment("A=1 VAR=\"1 NOT_A_VAR=2\" VBR='vbr'")
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {"A": "1", "VAR": "1 NOT_A_VAR=2", "VBR": "vbr"}
|
assert environment_dict == {"A": "1", "VAR": "1 NOT_A_VAR=2", "VBR": "vbr"}
|
||||||
assert environment_cmds == ["export A=1", 'export VAR="1 NOT_A_VAR=2"', "export VBR='vbr'"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_inheritance():
|
def test_inheritance():
|
||||||
environment_recipe = parse_environment("PATH=$PATH:/usr/local/bin")
|
environment_recipe = parse_environment("PATH=$PATH:/usr/local/bin")
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={"PATH": "/usr/bin"})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={"PATH": "/usr/bin"})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {"PATH": "/usr/bin:/usr/local/bin"}
|
assert environment_dict == {"PATH": "/usr/bin:/usr/local/bin"}
|
||||||
assert environment_cmds == ["export PATH=$PATH:/usr/local/bin"]
|
|
||||||
|
|
||||||
|
|
||||||
def test_shell_eval():
|
def test_shell_eval():
|
||||||
@@ -40,40 +34,32 @@ def test_shell_eval():
|
|||||||
env_copy.pop("VAR", None)
|
env_copy.pop("VAR", None)
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment=env_copy)
|
environment_dict = environment_recipe.as_dictionary(prev_environment=env_copy)
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict["VAR"] == "a test string"
|
assert environment_dict["VAR"] == "a test string"
|
||||||
assert environment_cmds == ['export VAR="$(echo "a test" string)"']
|
|
||||||
|
|
||||||
|
|
||||||
def test_shell_eval_and_env():
|
def test_shell_eval_and_env():
|
||||||
environment_recipe = parse_environment('VAR="$(echo "$PREV_VAR" string)"')
|
environment_recipe = parse_environment('VAR="$(echo "$PREV_VAR" string)"')
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={"PREV_VAR": "1 2 3"})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={"PREV_VAR": "1 2 3"})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {"PREV_VAR": "1 2 3", "VAR": "1 2 3 string"}
|
assert environment_dict == {"PREV_VAR": "1 2 3", "VAR": "1 2 3 string"}
|
||||||
assert environment_cmds == ['export VAR="$(echo "$PREV_VAR" string)"']
|
|
||||||
|
|
||||||
|
|
||||||
def test_empty_var():
|
def test_empty_var():
|
||||||
environment_recipe = parse_environment("CFLAGS=")
|
environment_recipe = parse_environment("CFLAGS=")
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={"CFLAGS": "-Wall"})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={"CFLAGS": "-Wall"})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {"CFLAGS": ""}
|
assert environment_dict == {"CFLAGS": ""}
|
||||||
assert environment_cmds == ["export CFLAGS="]
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_vars():
|
def test_no_vars():
|
||||||
environment_recipe = parse_environment("")
|
environment_recipe = parse_environment("")
|
||||||
|
|
||||||
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
environment_dict = environment_recipe.as_dictionary(prev_environment={})
|
||||||
environment_cmds = environment_recipe.as_shell_commands()
|
|
||||||
|
|
||||||
assert environment_dict == {}
|
assert environment_dict == {}
|
||||||
assert environment_cmds == []
|
|
||||||
|
|
||||||
|
|
||||||
def test_no_vars_pass_through():
|
def test_no_vars_pass_through():
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import platform as platform_module
|
import platform as platform_module
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
from cibuildwheel.__main__ import get_build_identifiers
|
from cibuildwheel.__main__ import get_build_identifiers
|
||||||
from cibuildwheel.environment import parse_environment
|
from cibuildwheel.environment import parse_environment
|
||||||
from cibuildwheel.options import Options, _get_pinned_docker_images
|
from cibuildwheel.options import Options, _get_pinned_docker_images
|
||||||
@@ -15,6 +17,8 @@ test-command = "pyproject"
|
|||||||
|
|
||||||
manylinux-x86_64-image = "manylinux1"
|
manylinux-x86_64-image = "manylinux1"
|
||||||
|
|
||||||
|
environment-pass = ["EXAMPLE_ENV"]
|
||||||
|
|
||||||
[tool.cibuildwheel.macos]
|
[tool.cibuildwheel.macos]
|
||||||
test-requires = "else"
|
test-requires = "else"
|
||||||
|
|
||||||
@@ -66,3 +70,47 @@ test_command: 'pyproject'
|
|||||||
assert local.manylinux_images is not None
|
assert local.manylinux_images is not None
|
||||||
assert local.test_command == "pyproject-override"
|
assert local.test_command == "pyproject-override"
|
||||||
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux2014"]
|
assert local.manylinux_images["x86_64"] == pinned_x86_64_docker_image["manylinux2014"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_passthrough(tmp_path, monkeypatch):
|
||||||
|
with tmp_path.joinpath("pyproject.toml").open("w") as f:
|
||||||
|
f.write(PYPROJECT_1)
|
||||||
|
|
||||||
|
args = get_default_command_line_arguments()
|
||||||
|
args.package_dir = str(tmp_path)
|
||||||
|
|
||||||
|
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setenv("EXAMPLE_ENV", "ONE")
|
||||||
|
|
||||||
|
options = Options(platform="linux", command_line_arguments=args)
|
||||||
|
|
||||||
|
default_build_options = options.build_options(identifier=None)
|
||||||
|
|
||||||
|
assert default_build_options.environment.as_dictionary(prev_environment={}) == {
|
||||||
|
"FOO": "BAR",
|
||||||
|
"EXAMPLE_ENV": "ONE",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"env_var_value",
|
||||||
|
[
|
||||||
|
"normal value",
|
||||||
|
'"value wrapped in quotes"',
|
||||||
|
"an unclosed single-quote: '",
|
||||||
|
'an unclosed double-quote: "',
|
||||||
|
"string\nwith\ncarriage\nreturns\n",
|
||||||
|
"a trailing backslash \\",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_passthrough_evil(tmp_path, monkeypatch, env_var_value):
|
||||||
|
args = get_default_command_line_arguments()
|
||||||
|
args.package_dir = str(tmp_path)
|
||||||
|
|
||||||
|
monkeypatch.setattr(platform_module, "machine", lambda: "x86_64")
|
||||||
|
monkeypatch.setenv("CIBW_ENVIRONMENT_PASS_LINUX", "ENV_VAR")
|
||||||
|
options = Options(platform="linux", command_line_arguments=args)
|
||||||
|
|
||||||
|
monkeypatch.setenv("ENV_VAR", env_var_value)
|
||||||
|
parsed_environment = options.build_options(identifier=None).environment
|
||||||
|
assert parsed_environment.as_dictionary(prev_environment={}) == {"ENV_VAR": env_var_value}
|
||||||
|
|||||||
Reference in New Issue
Block a user