Skip to content

Commit 76dba0b

Browse files
authored
Merge pull request #1588 from pypa/frontend-flags
Add the ability to pass extra flags to a build frontend through CIBW_BUILD_FRONTEND
2 parents 0954ffa + 5311f88 commit 76dba0b

File tree

10 files changed

+190
-48
lines changed

10 files changed

+190
-48
lines changed

cibuildwheel/linux.py

+6-5
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from .typing import PathOrStr
1717
from .util import (
1818
AlreadyBuiltWheelError,
19+
BuildFrontendConfig,
1920
BuildSelector,
2021
NonPlatformWheelError,
21-
build_frontend_or_default,
2222
find_compatible_wheel,
2323
get_build_verbosity_extra_flags,
2424
prepare_command,
@@ -177,7 +177,7 @@ def build_in_container(
177177
for config in platform_configs:
178178
log.build_start(config.identifier)
179179
build_options = options.build_options(config.identifier)
180-
build_frontend = build_frontend_or_default(build_options.build_frontend)
180+
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
181181

182182
dependency_constraint_flags: list[PathOrStr] = []
183183

@@ -243,9 +243,10 @@ def build_in_container(
243243
container.call(["rm", "-rf", built_wheel_dir])
244244
container.call(["mkdir", "-p", built_wheel_dir])
245245

246-
extra_flags = split_config_settings(build_options.config_settings, build_frontend)
246+
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
247+
extra_flags += build_frontend.args
247248

248-
if build_frontend == "pip":
249+
if build_frontend.name == "pip":
249250
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
250251
container.call(
251252
[
@@ -260,7 +261,7 @@ def build_in_container(
260261
],
261262
env=env,
262263
)
263-
elif build_frontend == "build":
264+
elif build_frontend.name == "build":
264265
if not 0 <= build_options.build_verbosity < 2:
265266
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
266267
log.warning(msg)

cibuildwheel/macos.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
from .util import (
2626
CIBW_CACHE_PATH,
2727
AlreadyBuiltWheelError,
28-
BuildFrontend,
28+
BuildFrontendConfig,
29+
BuildFrontendName,
2930
BuildSelector,
3031
NonPlatformWheelError,
31-
build_frontend_or_default,
3232
call,
3333
detect_ci_provider,
3434
download,
@@ -165,7 +165,7 @@ def setup_python(
165165
python_configuration: PythonConfiguration,
166166
dependency_constraint_flags: Sequence[PathOrStr],
167167
environment: ParsedEnvironment,
168-
build_frontend: BuildFrontend,
168+
build_frontend: BuildFrontendName,
169169
) -> dict[str, str]:
170170
tmp.mkdir()
171171
implementation_id = python_configuration.identifier.split("-")[0]
@@ -334,7 +334,7 @@ def build(options: Options, tmp_path: Path) -> None:
334334

335335
for config in python_configurations:
336336
build_options = options.build_options(config.identifier)
337-
build_frontend = build_frontend_or_default(build_options.build_frontend)
337+
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
338338
log.build_start(config.identifier)
339339

340340
identifier_tmp_dir = tmp_path / config.identifier
@@ -357,7 +357,7 @@ def build(options: Options, tmp_path: Path) -> None:
357357
config,
358358
dependency_constraint_flags,
359359
build_options.environment,
360-
build_frontend,
360+
build_frontend.name,
361361
)
362362

363363
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
@@ -378,9 +378,12 @@ def build(options: Options, tmp_path: Path) -> None:
378378
log.step("Building wheel...")
379379
built_wheel_dir.mkdir()
380380

381-
extra_flags = split_config_settings(build_options.config_settings, build_frontend)
381+
extra_flags = split_config_settings(
382+
build_options.config_settings, build_frontend.name
383+
)
384+
extra_flags += build_frontend.args
382385

383-
if build_frontend == "pip":
386+
if build_frontend.name == "pip":
384387
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
385388
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
386389
# see https://github.com/pypa/cibuildwheel/pull/369
@@ -395,7 +398,7 @@ def build(options: Options, tmp_path: Path) -> None:
395398
*extra_flags,
396399
env=env,
397400
)
398-
elif build_frontend == "build":
401+
elif build_frontend.name == "build":
399402
if not 0 <= build_options.build_verbosity < 2:
400403
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
401404
log.warning(msg)

cibuildwheel/oci_container.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ class OCIContainerEngineConfig:
2929

3030
@staticmethod
3131
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
32-
config_dict = parse_key_value_string(config_string, ["name"])
32+
config_dict = parse_key_value_string(
33+
config_string, ["name"], ["create_args", "create-args"]
34+
)
3335
name = " ".join(config_dict["name"])
3436
if name not in {"docker", "podman"}:
3537
msg = f"unknown container engine {name}"

cibuildwheel/options.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .util import (
2828
MANYLINUX_ARCHS,
2929
MUSLLINUX_ARCHS,
30-
BuildFrontend,
30+
BuildFrontendConfig,
3131
BuildSelector,
3232
DependencyConstraints,
3333
TestSelector,
@@ -92,7 +92,7 @@ class BuildOptions:
9292
test_requires: list[str]
9393
test_extras: str
9494
build_verbosity: int
95-
build_frontend: BuildFrontend | Literal["default"]
95+
build_frontend: BuildFrontendConfig | None
9696
config_settings: str
9797

9898
@property
@@ -488,7 +488,6 @@ def build_options(self, identifier: str | None) -> BuildOptions:
488488
with self.reader.identifier(identifier):
489489
before_all = self.reader.get("before-all", sep=" && ")
490490

491-
build_frontend_str = self.reader.get("build-frontend", env_plat=False)
492491
environment_config = self.reader.get(
493492
"environment", table={"item": '{k}="{v}"', "sep": " "}
494493
)
@@ -506,17 +505,20 @@ def build_options(self, identifier: str | None) -> BuildOptions:
506505
test_extras = self.reader.get("test-extras", sep=",")
507506
build_verbosity_str = self.reader.get("build-verbosity")
508507

509-
build_frontend: BuildFrontend | Literal["default"]
510-
if build_frontend_str == "build":
511-
build_frontend = "build"
512-
elif build_frontend_str == "pip":
513-
build_frontend = "pip"
514-
elif build_frontend_str == "default":
515-
build_frontend = "default"
508+
build_frontend_str = self.reader.get(
509+
"build-frontend",
510+
env_plat=False,
511+
table={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
512+
)
513+
build_frontend: BuildFrontendConfig | None
514+
if not build_frontend_str or build_frontend_str == "default":
515+
build_frontend = None
516516
else:
517-
msg = f"cibuildwheel: Unrecognised build frontend {build_frontend_str!r}, only 'pip' and 'build' are supported"
518-
print(msg, file=sys.stderr)
519-
sys.exit(2)
517+
try:
518+
build_frontend = BuildFrontendConfig.from_config_string(build_frontend_str)
519+
except ValueError as e:
520+
print(f"cibuildwheel: {e}", file=sys.stderr)
521+
sys.exit(2)
520522

521523
try:
522524
environment = parse_environment(environment_config)

cibuildwheel/util.py

+38-11
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,6 @@
5757

5858
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py"
5959

60-
BuildFrontend = Literal["pip", "build"]
61-
62-
63-
def build_frontend_or_default(
64-
setting: BuildFrontend | Literal["default"], default: BuildFrontend = "pip"
65-
) -> BuildFrontend:
66-
if setting == "default":
67-
return default
68-
return setting
69-
7060

7161
MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
7262
"x86_64",
@@ -376,6 +366,34 @@ def options_summary(self) -> Any:
376366
return self.base_file_path.name
377367

378368

369+
BuildFrontendName = Literal["pip", "build"]
370+
371+
372+
@dataclass(frozen=True)
373+
class BuildFrontendConfig:
374+
name: BuildFrontendName
375+
args: Sequence[str] = ()
376+
377+
@staticmethod
378+
def from_config_string(config_string: str) -> BuildFrontendConfig:
379+
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
380+
name = " ".join(config_dict["name"])
381+
if name not in {"pip", "build"}:
382+
msg = f"Unrecognised build frontend {name}, only 'pip' and 'build' are supported"
383+
raise ValueError(msg)
384+
385+
name = typing.cast(BuildFrontendName, name)
386+
387+
args = config_dict.get("args") or []
388+
return BuildFrontendConfig(name=name, args=args)
389+
390+
def options_summary(self) -> str | dict[str, str]:
391+
if not self.args:
392+
return self.name
393+
else:
394+
return {"name": self.name, "args": repr(self.args)}
395+
396+
379397
class NonPlatformWheelError(Exception):
380398
def __init__(self) -> None:
381399
message = textwrap.dedent(
@@ -699,13 +717,19 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
699717

700718

701719
def parse_key_value_string(
702-
key_value_string: str, positional_arg_names: list[str] | None = None
720+
key_value_string: str,
721+
positional_arg_names: Sequence[str] | None = None,
722+
kw_arg_names: Sequence[str] | None = None,
703723
) -> dict[str, list[str]]:
704724
"""
705725
Parses a string like "docker; create_args: --some-option=value another-option"
706726
"""
707727
if positional_arg_names is None:
708728
positional_arg_names = []
729+
if kw_arg_names is None:
730+
kw_arg_names = []
731+
732+
all_field_names = [*positional_arg_names, *kw_arg_names]
709733

710734
shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";:")
711735
shlexer.commenters = ""
@@ -721,6 +745,9 @@ def parse_key_value_string(
721745
if len(field) > 1 and field[1] == ":":
722746
field_name = field[0]
723747
values = field[2:]
748+
if field_name not in all_field_names:
749+
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
750+
raise ValueError(msg)
724751
else:
725752
try:
726753
field_name = positional_arg_names[field_i]

cibuildwheel/windows.py

+11-8
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@
2525
from .util import (
2626
CIBW_CACHE_PATH,
2727
AlreadyBuiltWheelError,
28-
BuildFrontend,
28+
BuildFrontendConfig,
29+
BuildFrontendName,
2930
BuildSelector,
3031
NonPlatformWheelError,
31-
build_frontend_or_default,
3232
call,
3333
download,
3434
find_compatible_wheel,
@@ -216,7 +216,7 @@ def setup_python(
216216
python_configuration: PythonConfiguration,
217217
dependency_constraint_flags: Sequence[PathOrStr],
218218
environment: ParsedEnvironment,
219-
build_frontend: BuildFrontend,
219+
build_frontend: BuildFrontendName,
220220
) -> dict[str, str]:
221221
tmp.mkdir()
222222
implementation_id = python_configuration.identifier.split("-")[0]
@@ -369,7 +369,7 @@ def build(options: Options, tmp_path: Path) -> None:
369369

370370
for config in python_configurations:
371371
build_options = options.build_options(config.identifier)
372-
build_frontend = build_frontend_or_default(build_options.build_frontend)
372+
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
373373
log.build_start(config.identifier)
374374

375375
identifier_tmp_dir = tmp_path / config.identifier
@@ -390,7 +390,7 @@ def build(options: Options, tmp_path: Path) -> None:
390390
config,
391391
dependency_constraint_flags,
392392
build_options.environment,
393-
build_frontend,
393+
build_frontend.name,
394394
)
395395

396396
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
@@ -414,9 +414,12 @@ def build(options: Options, tmp_path: Path) -> None:
414414
log.step("Building wheel...")
415415
built_wheel_dir.mkdir()
416416

417-
extra_flags = split_config_settings(build_options.config_settings, build_frontend)
417+
extra_flags = split_config_settings(
418+
build_options.config_settings, build_frontend.name
419+
)
420+
extra_flags += build_frontend.args
418421

419-
if build_frontend == "pip":
422+
if build_frontend.name == "pip":
420423
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
421424
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
422425
# see https://github.com/pypa/cibuildwheel/pull/369
@@ -431,7 +434,7 @@ def build(options: Options, tmp_path: Path) -> None:
431434
*extra_flags,
432435
env=env,
433436
)
434-
elif build_frontend == "build":
437+
elif build_frontend.name == "build":
435438
if not 0 <= build_options.build_verbosity < 2:
436439
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
437440
log.warning(msg)

docs/options.md

+17-1
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,19 @@ This option can also be set using the [command-line option](#command-line) `--pr
504504
### `CIBW_BUILD_FRONTEND` {: #build-frontend}
505505
> Set the tool to use to build, either "pip" (default for now) or "build"
506506
507-
Choose which build backend to use. Can either be "pip", which will run
507+
Options:
508+
509+
- `pip[;args: ...]`
510+
- `build[;args: ...]`
511+
512+
Default: `pip`
513+
514+
Choose which build frontend to use. Can either be "pip", which will run
508515
`python -m pip wheel`, or "build", which will run `python -m build --wheel`.
509516

517+
You can specify extra arguments to pass to `pip wheel` or `build` using the
518+
optional `args` option.
519+
510520
!!! tip
511521
Until v2.0.0, [pip] was the only way to build wheels, and is still the
512522
default. However, we expect that at some point in the future, cibuildwheel
@@ -526,6 +536,9 @@ Choose which build backend to use. Can either be "pip", which will run
526536

527537
# Ensure pip is used even if the default changes in the future
528538
CIBW_BUILD_FRONTEND: "pip"
539+
540+
# supply an extra argument to 'pip wheel'
541+
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
529542
```
530543

531544
!!! tab examples "pyproject.toml"
@@ -537,6 +550,9 @@ Choose which build backend to use. Can either be "pip", which will run
537550

538551
# Ensure pip is used even if the default changes in the future
539552
build-frontend = "pip"
553+
554+
# supply an extra argument to 'pip wheel'
555+
build-frontend = { name = "pip", args = ["--no-build-isolation"] }
540556
```
541557

542558
### `CIBW_CONFIG_SETTINGS` {: #config-settings}

test/test_build_frontend_args.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import subprocess
2+
3+
import pytest
4+
5+
from . import utils
6+
from .test_projects.c import new_c_project
7+
8+
9+
@pytest.mark.parametrize("frontend_name", ["pip", "build"])
10+
def test_build_frontend_args(tmp_path, capfd, frontend_name):
11+
project = new_c_project()
12+
project_dir = tmp_path / "project"
13+
project.generate(project_dir)
14+
15+
# the build will fail because the frontend is called with '-h' - it prints the help message
16+
with pytest.raises(subprocess.CalledProcessError):
17+
utils.cibuildwheel_run(
18+
project_dir,
19+
add_env={
20+
"CIBW_BUILD": "cp311-*",
21+
"CIBW_BUILD_FRONTEND": f"{frontend_name}; args: -h",
22+
},
23+
)
24+
25+
captured = capfd.readouterr()
26+
print(captured.out)
27+
28+
# check that the help message was printed
29+
if frontend_name == "pip":
30+
assert "Usage:" in captured.out
31+
assert "Wheel Options:" in captured.out
32+
else:
33+
assert "usage:" in captured.out
34+
assert "A simple, correct Python build frontend." in captured.out

0 commit comments

Comments
 (0)