Skip to content

Commit 3597095

Browse files
authored
feat: support config-settings (#1244)
* feat: support config-settings Signed-off-by: Henry Schreiner <[email protected]> feat: support config-settings Signed-off-by: Henry Schreiner <[email protected]> * refactor: use shlex.quote Signed-off-by: Henry Schreiner <[email protected]> Signed-off-by: Henry Schreiner <[email protected]>
1 parent 6a9b39a commit 3597095

File tree

10 files changed

+170
-16
lines changed

10 files changed

+170
-16
lines changed

cibuildwheel/linux.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
get_build_verbosity_extra_flags,
2121
prepare_command,
2222
read_python_configs,
23+
split_config_settings,
2324
unwrap,
2425
)
2526

@@ -212,8 +213,10 @@ def build_in_container(
212213
container.call(["mkdir", "-p", built_wheel_dir])
213214

214215
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
216+
extra_flags = split_config_settings(build_options.config_settings)
215217

216218
if build_options.build_frontend == "pip":
219+
extra_flags += verbosity_flags
217220
container.call(
218221
[
219222
"python",
@@ -223,12 +226,13 @@ def build_in_container(
223226
container_package_dir,
224227
f"--wheel-dir={built_wheel_dir}",
225228
"--no-deps",
226-
*verbosity_flags,
229+
*extra_flags,
227230
],
228231
env=env,
229232
)
230233
elif build_options.build_frontend == "build":
231-
config_setting = " ".join(verbosity_flags)
234+
verbosity_setting = " ".join(verbosity_flags)
235+
extra_flags += (f"--config-setting={verbosity_setting}",)
232236
container.call(
233237
[
234238
"python",
@@ -237,7 +241,7 @@ def build_in_container(
237241
container_package_dir,
238242
"--wheel",
239243
f"--outdir={built_wheel_dir}",
240-
f"--config-setting={config_setting}",
244+
*extra_flags,
241245
],
242246
env=env,
243247
)

cibuildwheel/macos.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
prepare_command,
3535
read_python_configs,
3636
shell,
37+
split_config_settings,
3738
unwrap,
3839
virtualenv,
3940
)
@@ -345,8 +346,10 @@ def build(options: Options, tmp_path: Path) -> None:
345346
built_wheel_dir.mkdir()
346347

347348
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
349+
extra_flags = split_config_settings(build_options.config_settings)
348350

349351
if build_options.build_frontend == "pip":
352+
extra_flags += verbosity_flags
350353
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
351354
# see https://github.com/pypa/cibuildwheel/pull/369
352355
call(
@@ -357,11 +360,12 @@ def build(options: Options, tmp_path: Path) -> None:
357360
build_options.package_dir.resolve(),
358361
f"--wheel-dir={built_wheel_dir}",
359362
"--no-deps",
360-
*verbosity_flags,
363+
*extra_flags,
361364
env=env,
362365
)
363366
elif build_options.build_frontend == "build":
364-
config_setting = " ".join(verbosity_flags)
367+
verbosity_setting = " ".join(verbosity_flags)
368+
extra_flags += (f"--config-setting={verbosity_setting}",)
365369
build_env = env.copy()
366370
if build_options.dependency_constraints:
367371
constraint_path = (
@@ -378,7 +382,7 @@ def build(options: Options, tmp_path: Path) -> None:
378382
build_options.package_dir,
379383
"--wheel",
380384
f"--outdir={built_wheel_dir}",
381-
f"--config-setting={config_setting}",
385+
*extra_flags,
382386
env=build_env,
383387
)
384388
else:

cibuildwheel/options.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
import difflib
44
import functools
55
import os
6+
import shlex
67
import sys
78
import traceback
89
from configparser import ConfigParser
910
from contextlib import contextmanager
1011
from dataclasses import asdict, dataclass
1112
from pathlib import Path
12-
from typing import Any, Dict, Generator, List, Mapping, Union, cast
13+
from typing import Any, Dict, Generator, Iterator, List, Mapping, Union, cast
1314

1415
if sys.version_info >= (3, 11):
1516
import tomllib
@@ -77,6 +78,7 @@ class BuildOptions:
7778
test_extras: str
7879
build_verbosity: int
7980
build_frontend: BuildFrontend
81+
config_settings: str
8082

8183
@property
8284
def package_dir(self) -> Path:
@@ -293,8 +295,9 @@ def get(
293295
accept platform versions of the environment variable. If this is an
294296
array it will be merged with "sep" before returning. If it is a table,
295297
it will be formatted with "table['item']" using {k} and {v} and merged
296-
with "table['sep']". Empty variables will not override if ignore_empty
297-
is True.
298+
with "table['sep']". If sep is also given, it will be used for arrays
299+
inside the table (must match table['sep']). Empty variables will not
300+
override if ignore_empty is True.
298301
"""
299302

300303
if name not in self.default_options and name not in self.default_platform_options:
@@ -324,7 +327,9 @@ def get(
324327
if isinstance(result, dict):
325328
if table is None:
326329
raise ConfigOptionError(f"{name!r} does not accept a table")
327-
return table["sep"].join(table["item"].format(k=k, v=v) for k, v in result.items())
330+
return table["sep"].join(
331+
item for k, v in result.items() for item in _inner_fmt(k, v, table["item"])
332+
)
328333

329334
if isinstance(result, list):
330335
if sep is None:
@@ -337,6 +342,16 @@ def get(
337342
return result
338343

339344

345+
def _inner_fmt(k: str, v: Any, table_item: str) -> Iterator[str]:
346+
if isinstance(v, list):
347+
for inner_v in v:
348+
qv = shlex.quote(inner_v)
349+
yield table_item.format(k=k, v=qv)
350+
else:
351+
qv = shlex.quote(v)
352+
yield table_item.format(k=k, v=qv)
353+
354+
340355
class Options:
341356
def __init__(self, platform: PlatformName, command_line_arguments: CommandLineArguments):
342357
self.platform = platform
@@ -427,11 +442,14 @@ def build_options(self, identifier: str | None) -> BuildOptions:
427442

428443
build_frontend_str = self.reader.get("build-frontend", env_plat=False)
429444
environment_config = self.reader.get(
430-
"environment", table={"item": '{k}="{v}"', "sep": " "}
445+
"environment", table={"item": "{k}={v}", "sep": " "}
431446
)
432447
environment_pass = self.reader.get("environment-pass", sep=" ").split()
433448
before_build = self.reader.get("before-build", sep=" && ")
434449
repair_command = self.reader.get("repair-wheel-command", sep=" && ")
450+
config_settings = self.reader.get(
451+
"config-settings", table={"item": "{k}={v}", "sep": " "}
452+
)
435453

436454
dependency_versions = self.reader.get("dependency-versions")
437455
test_command = self.reader.get("test-command", sep=" && ")
@@ -537,6 +555,7 @@ def build_options(self, identifier: str | None) -> BuildOptions:
537555
manylinux_images=manylinux_images or None,
538556
musllinux_images=musllinux_images or None,
539557
build_frontend=build_frontend,
558+
config_settings=config_settings,
540559
)
541560

542561
def check_for_invalid_configuration(self, identifiers: list[str]) -> None:

cibuildwheel/resources/defaults.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ test-skip = ""
55

66
archs = ["auto"]
77
build-frontend = "pip"
8+
config-settings = {}
89
dependency-versions = "pinned"
910
environment = {}
1011
environment-pass = []

cibuildwheel/util.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
"strtobool",
6161
"cached_property",
6262
"chdir",
63+
"split_config_settings",
6364
]
6465

6566
resources_dir: Final = Path(__file__).parent / "resources"
@@ -205,6 +206,11 @@ def get_build_verbosity_extra_flags(level: int) -> list[str]:
205206
return []
206207

207208

209+
def split_config_settings(config_settings: str) -> list[str]:
210+
config_settings_list = shlex.split(config_settings)
211+
return [f"--config-setting={setting}" for setting in config_settings_list]
212+
213+
208214
def read_python_configs(config: PlatformName) -> list[dict[str, str]]:
209215
input_file = resources_dir / "build-platforms.toml"
210216
with input_file.open("rb") as f:

cibuildwheel/windows.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
prepare_command,
3333
read_python_configs,
3434
shell,
35+
split_config_settings,
3536
virtualenv,
3637
)
3738

@@ -302,8 +303,10 @@ def build(options: Options, tmp_path: Path) -> None:
302303
built_wheel_dir.mkdir()
303304

304305
verbosity_flags = get_build_verbosity_extra_flags(build_options.build_verbosity)
306+
extra_flags = split_config_settings(build_options.config_settings)
305307

306308
if build_options.build_frontend == "pip":
309+
extra_flags += verbosity_flags
307310
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
308311
# see https://github.com/pypa/cibuildwheel/pull/369
309312
call(
@@ -314,11 +317,12 @@ def build(options: Options, tmp_path: Path) -> None:
314317
options.globals.package_dir.resolve(),
315318
f"--wheel-dir={built_wheel_dir}",
316319
"--no-deps",
317-
*get_build_verbosity_extra_flags(build_options.build_verbosity),
320+
*extra_flags,
318321
env=env,
319322
)
320323
elif build_options.build_frontend == "build":
321-
config_setting = " ".join(verbosity_flags)
324+
verbosity_setting = " ".join(verbosity_flags)
325+
extra_flags += (f"--config-setting={verbosity_setting}",)
322326
build_env = env.copy()
323327
if build_options.dependency_constraints:
324328
constraints_path = (
@@ -345,7 +349,7 @@ def build(options: Options, tmp_path: Path) -> None:
345349
build_options.package_dir,
346350
"--wheel",
347351
f"--outdir={built_wheel_dir}",
348-
f"--config-setting={config_setting}",
352+
*extra_flags,
349353
env=build_env,
350354
)
351355
else:

docs/options.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,36 @@ Choose which build backend to use. Can either be "pip", which will run
529529
build-frontend = "pip"
530530
```
531531

532+
### `CIBW_CONFIG_SETTINGS` {: #config-settings}
533+
> Specify config-settings for the build backend.
534+
535+
Specify config settings for the build backend. Each space separated
536+
item will be passed via `--config-setting`. In TOML, you can specify
537+
a table of items, including arrays.
538+
539+
!!! tip
540+
Currently, "build" supports arrays for options, but "pip" only supports
541+
single values.
542+
543+
Platform-specific environment variables also available:<br/>
544+
`CIBW_BEFORE_ALL_MACOS` | `CIBW_BEFORE_ALL_WINDOWS` | `CIBW_BEFORE_ALL_LINUX`
545+
546+
547+
#### Examples
548+
549+
!!! tab examples "Environment variables"
550+
551+
```yaml
552+
CIBW_CONFIG_SETTINGS: "--build-option=--use-mypyc"
553+
```
554+
555+
!!! tab examples "pyproject.toml"
556+
557+
```toml
558+
[tool.cibuildwheel.config-settings]
559+
--build-option = "--use-mypyc"
560+
```
561+
532562

533563
### `CIBW_ENVIRONMENT` {: #environment}
534564
> Set environment variables needed during the build

unit_test/main_tests/main_options_test.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from cibuildwheel.__main__ import main
1515
from cibuildwheel.environment import ParsedEnvironment
1616
from cibuildwheel.options import BuildOptions, _get_pinned_container_images
17-
from cibuildwheel.util import BuildSelector, resources_dir
17+
from cibuildwheel.util import BuildSelector, resources_dir, split_config_settings
1818

1919
# CIBW_PLATFORM is tested in main_platform_test.py
2020

@@ -263,6 +263,27 @@ def test_build_verbosity(
263263
assert build_options.build_verbosity == expected_verbosity
264264

265265

266+
@pytest.mark.parametrize("platform_specific", [False, True])
267+
def test_config_settings(platform_specific, platform, intercepted_build_args, monkeypatch):
268+
config_settings = 'setting=value setting=value2 other="something else"'
269+
if platform_specific:
270+
monkeypatch.setenv("CIBW_CONFIG_SETTINGS_" + platform.upper(), config_settings)
271+
monkeypatch.setenv("CIBW_CONFIG_SETTIGNS", "a=b")
272+
else:
273+
monkeypatch.setenv("CIBW_CONFIG_SETTINGS", config_settings)
274+
275+
main()
276+
build_options = intercepted_build_args.args[0].build_options(identifier=None)
277+
278+
assert build_options.config_settings == config_settings
279+
280+
assert split_config_settings(config_settings) == [
281+
"--config-setting=setting=value",
282+
"--config-setting=setting=value2",
283+
"--config-setting=other=something else",
284+
]
285+
286+
266287
@pytest.mark.parametrize(
267288
"selector",
268289
[

unit_test/options_test.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import platform as platform_module
4+
import textwrap
45

56
import pytest
67

@@ -58,7 +59,7 @@ def test_options_1(tmp_path, monkeypatch):
5859

5960
default_build_options = options.build_options(identifier=None)
6061

61-
assert default_build_options.environment == parse_environment('FOO="BAR"')
62+
assert default_build_options.environment == parse_environment("FOO=BAR")
6263

6364
all_pinned_container_images = _get_pinned_container_images()
6465
pinned_x86_64_container_image = all_pinned_container_images["x86_64"]
@@ -116,3 +117,32 @@ def test_passthrough_evil(tmp_path, monkeypatch, env_var_value):
116117
monkeypatch.setenv("ENV_VAR", env_var_value)
117118
parsed_environment = options.build_options(identifier=None).environment
118119
assert parsed_environment.as_dictionary(prev_environment={}) == {"ENV_VAR": env_var_value}
120+
121+
122+
@pytest.mark.parametrize(
123+
"env_var_value",
124+
[
125+
"normal value",
126+
'"value wrapped in quotes"',
127+
'an unclosed double-quote: "',
128+
"string\nwith\ncarriage\nreturns\n",
129+
"a trailing backslash \\",
130+
],
131+
)
132+
def test_toml_environment_evil(tmp_path, monkeypatch, env_var_value):
133+
args = get_default_command_line_arguments()
134+
args.package_dir = tmp_path
135+
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+
)
145+
146+
options = Options(platform="linux", command_line_arguments=args)
147+
parsed_environment = options.build_options(identifier=None).environment
148+
assert parsed_environment.as_dictionary(prev_environment={}) == {"EXAMPLE": env_var_value}

0 commit comments

Comments
 (0)