8000 Merge pull request #1273 from pypa/toml-environment-quoting · pypa/cibuildwheel@ad17269 · GitHub
[go: up one dir, main page]

Skip to content

Commit ad17269

Browse files
authored
Merge pull request #1273 from pypa/toml-environment-quoting
2 parents 00b2600 + 5dcb363 commit ad17269

File tree

6 files changed

+86
-24
lines changed

6 files changed

+86
-24
lines changed

cibuildwheel/environment.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import Any, Mapping, Sequence
55

66
import bashlex
7+
import bashlex.errors
78

89
from cibuildwheel.typing import Protocol
910

@@ -33,7 +34,11 @@ def split_env_items(env_string: str) -> list[str]:
3334
if not env_string:
3435
return []
3536

36-
command_node = bashlex.parsesingle(env_string)
37+
try:
38+
command_node = bashlex.parsesingle(env_string)
39+
except bashlex.errors.ParsingError as e:
40+
raise EnvironmentParseError(env_string) from e
41+
3742
result = []
3843

3944
for word_node in command_node.parts:

cibuildwheel/options.py

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from contextlib import contextmanager
1111
from dataclasses import asdict, dataclass
1212
from pathlib import Path
13-
from typing import Any, Dict, Generator, Iterator, List, Mapping, Union, cast
13+
from typing import Any, Callable, Dict, Generator, Iterator, List, Mapping, Union, cast
1414

1515
if sys.version_info >= (3, 11):
1616
import tomllib
@@ -23,7 +23,7 @@
2323
from .environment import EnvironmentParseError, ParsedEnvironment, parse_environment
2424
from .oci_container import ContainerEngine
2525
from .projectfiles import get_requires_python_str
26-
from .typing import PLATFORMS, Literal, PlatformName, TypedDict
26+
from .typing import PLATFORMS, Literal, NotRequired, PlatformName, TypedDict
2727
from .util import (
2828
MANYLINUX_ARCHS,
2929
MUSLLINUX_ARCHS,
@@ -123,6 +123,7 @@ class Override:
123123
class TableFmt(TypedDict):
124124
item: str
125125
sep: str
126+
quote: NotRequired[Callable[[str], str]]
126127

127128

128129
class ConfigOptionError(KeyError):
@@ -329,7 +330,7 @@ def get(
329330
if table is None:
330331
raise ConfigOptionError(f"{name!r} does not accept a table")
331332
return table["sep"].join(
332-
item for k, v in result.items() for item in _inner_fmt(k, v, table["item"])
333+
item for k, v in result.items() for item in _inner_fmt(k, v, table)
333334
)
334335

335336
if isinstance(result, list):
@@ -343,14 +344,16 @@ def get(
343344
return result
344345

345346

346-
def _inner_fmt(k: str, v: Any, table_item: str) -> Iterator[str]:
347+
def _inner_fmt(k: str, v: Any, table: TableFmt) -> Iterator[str]:
348+
quote_function = table.get("quote", lambda a: a)
349+
347350
if isinstance(v, list):
348351
for inner_v in v:
349-
qv = shlex.quote(inner_v)
350-
yield table_item.format(k=k, v=qv)
352+
qv = quote_function(inner_v)
353+
yield table["item"].format(k=k, v=qv)
351354
else:
352-
qv = shlex.quote(v)
353-
yield table_item.format(k=k, v=qv)
355+
qv = quote_function(v)
356+
yield table["item"].format(k=k, v=qv)
354357

355358

356359
class Options:
@@ -449,13 +452,13 @@ def build_options(self, identifier: str | None) -> BuildOptions:
449452

450453
build_frontend_str = self.reader.get("build-frontend", env_plat=False)
451454
environment_config = self.reader.get(
452-
"environment", table={"item": "{k}={v}", "sep": " "}
455+
"environment", table={"item": '{k}="{v}"', "sep": " "}
453456
)
454457
environment_pass = self.reader.get("environment-pass", sep=" ").split()
455458
before_build = self.reader.get("before-build", sep=" && ")
456459
repair_command = self.reader.get("repair-wheel-command", sep=" && ")
457460
config_settings = self.reader.get(
458-
"config-settings", table={"item": "{k}={v}", "sep": " "}
461+
"config-settings", table={"item": "{k}={v}", "sep": " ", "quote": shlex.quote}
459462
)
460463

461464
dependency_versions = self.reader.get("dependency-versions")

cibuildwheel/typing.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
else:
1111
from typing import Final, Literal, OrderedDict, Protocol, TypedDict
1212

13+
if sys.version_info < (3, 11):
14+
from typing_extensions import NotRequired
15+
else:
16+
from typing import NotRequired
1317

1418
__all__ = (
1519
"Final",
@@ -26,6 +30,7 @@
2630
"OrderedDict",
2731
"Union",
2832
"assert_never",
33+
"NotRequired",
2934
)
3035

3136

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ module = [
6666
"setuptools",
6767
"pytest", # ignored in pre-commit to speed up check
6868
"bashlex",
69+
"bashlex.*",
6970
"importlib_resources",
7071
"ghapi.*",
7172
]

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ install_requires =
3737
packaging>=20.9
3838
platformdirs
3939
tomli;python_version < '3.11'
40-
typing-extensions>=3.10.0.0;python_version < '3.8'
40+
typing-extensions>=4.1.0;python_version < '3.11'
4141
python_requires = >=3.7
4242
include_package_data = True
4343
zip_safe = False

unit_test/options_test.py

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
from __future__ import annotations
22

3+
import os
34
import platform as platform_module
45
import textwrap
6+
from pathlib import Path
57

68
import pytest
79

810
from cibuildwheel.__main__ import get_build_identifiers
11+
from cibuildwheel.bashlex_eval import local_environment_executor
912
from cibuildwheel.environment import parse_environment
1013
from cibuildwheel.options import Options, _get_pinned_container_images
1114

@@ -59,7 +62,7 @@ def test_options_1(tmp_path, monkeypatch):
5962

6063
default_build_options = options.build_options(identifier=None)
6164

62-
assert default_build_options.environment == parse_environment("FOO=BAR")
65+
assert default_build_options.environment == parse_environment('FOO="BAR"')
6366

6467
all_pinned_container_images = _get_pinned_container_images()
6568
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
@@ -119,30 +122,75 @@ def test_passthrough_evil(tmp_path, monkeypatch, env_var_value):
119122
assert parsed_environment.as_dictionary(prev_environment={}) == {"ENV_VAR": env_var_value}
120123

121124

125+
xfail_env_parse = pytest.mark.xfail(
126+
raises=SystemExit, reason="until we can figure out the right way to quote these values"
127+
)
128+
129+
122130
@pytest.mark.parametrize(
123131
"env_var_value",
124132
[
125133
"normal value",
126-
'"value wrapped in quotes"',
127-
'an unclosed double-quote: "',
134+
pytest.param('"value wrapped in quotes"', marks=[xfail_env_parse]),
135+
pytest.param('an unclosed double-quote: "', marks=[xfail_env_parse]),
128136
"string\nwith\ncarriage\nreturns\n",
129-
"a trailing backslash \\",
137+
pytest.param("a trailing backslash \\", marks=[xfail_env_parse]),
130138
],
131139
)
132140
def test_toml_environment_evil(tmp_path, monkeypatch, env_var_value):
133141
args = get_default_command_line_arguments()
134142
args.package_dir = tmp_path
135143

136-
with tmp_path.joinpath("pyproject.toml").open("w") as f:
137-
f.write(
138-
textwrap.dedent(
139-
f"""\
140-
[tool.cibuildwheel.environment]
141-
EXAMPLE='''{env_var_value}'''
142-
"""
143-
)
144+
tmp_path.joinpath("pyproject.toml").write_text(
145+
textwrap.dedent(
146+
f"""\
147+
[tool.cibuildwheel.environment]
148+
EXAMPLE='''{env_var_value}'''
149+
"""
144150
)
151+
)
145152

146153
options = Options(platform="linux", command_line_arguments=args)
147154
parsed_environment = options.build_options(identifier=None).environment
148155
assert parsed_environment.as_dictionary(prev_environment={}) == {"EXAMPLE": env_var_value}
156+
157+
158+
@pytest.mark.parametrize(
159+
"toml_assignment,result_value",
160+
[
161+
('TEST_VAR="simple_value"', "simple_value"),
162+
# spaces
163+
('TEST_VAR="simple value"', "simple value"),
164+
# env var
165+
('TEST_VAR="$PARAM"', "spam"),
166+
('TEST_VAR="$PARAM $PARAM"', "spam spam"),
167+
# env var extension
168+
('TEST_VAR="before:$PARAM:after"', "before:spam:after"),
169+
# env var extension with spaces
170+
('TEST_VAR="before $PARAM after"', "before spam after"),
171+
# literal $ - this test is just for reference, I'm not sure if this
172+
# syntax will work if we change the TOML quoting behaviour
173+
(r'TEST_VAR="before\\$after"', "before$after"),
174+
],
175+
)
176+
def test_toml_environment_quoting(tmp_path: Path, toml_assignment, result_value):
177+
args = get_default_command_line_arguments()
178+
args.package_dir = tmp_path
179+
180+
tmp_path.joinpath("pyproject.toml").write_text(
181+
textwrap.dedent(
182+
f"""\
183+
[tool.cibuildwheel.environment]
184+
{toml_assignment}
185+
"""
186+
)
187+
)
188+
189+
options = Options(platform="linux", command_line_arguments=args)
190+
parsed_environment = options.build_options(identifier=None).environment
191+
environment_values = parsed_environment.as_dictionary(
192+
prev_environment={**os.environ, "PARAM": "spam"},
193+
executor=local_environment_executor,
194+
)
195+
196+
assert environment_values["TEST_VAR"] == result_value

0 commit comments

Comments
 (0)
0