Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to pass extra flags to a build frontend through CIBW_BUILD_FRONTEND #1588

Merged
merged 4 commits into from Sep 18, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 6 additions & 5 deletions cibuildwheel/linux.py
Expand Up @@ -16,9 +16,9 @@
from .typing import PathOrStr
from .util import (
AlreadyBuiltWheelError,
BuildFrontendConfig,
BuildSelector,
NonPlatformWheelError,
build_frontend_or_default,
find_compatible_wheel,
get_build_verbosity_extra_flags,
prepare_command,
Expand Down Expand Up @@ -177,7 +177,7 @@ def build_in_container(
for config in platform_configs:
log.build_start(config.identifier)
build_options = options.build_options(config.identifier)
build_frontend = build_frontend_or_default(build_options.build_frontend)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")

dependency_constraint_flags: list[PathOrStr] = []

Expand Down Expand Up @@ -243,9 +243,10 @@ def build_in_container(
container.call(["rm", "-rf", built_wheel_dir])
container.call(["mkdir", "-p", built_wheel_dir])

extra_flags = split_config_settings(build_options.config_settings, build_frontend)
extra_flags = split_config_settings(build_options.config_settings, build_frontend.name)
extra_flags += build_frontend.args

if build_frontend == "pip":
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
container.call(
[
Expand All @@ -260,7 +261,7 @@ def build_in_container(
],
env=env,
)
elif build_frontend == "build":
elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
Expand Down
19 changes: 11 additions & 8 deletions cibuildwheel/macos.py
Expand Up @@ -25,10 +25,10 @@
from .util import (
CIBW_CACHE_PATH,
AlreadyBuiltWheelError,
BuildFrontend,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
NonPlatformWheelError,
build_frontend_or_default,
call,
detect_ci_provider,
download,
Expand Down Expand Up @@ -165,7 +165,7 @@ def setup_python(
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontend,
build_frontend: BuildFrontendName,
) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
Expand Down Expand Up @@ -334,7 +334,7 @@ def build(options: Options, tmp_path: Path) -> None:

for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_frontend_or_default(build_options.build_frontend)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
log.build_start(config.identifier)

identifier_tmp_dir = tmp_path / config.identifier
Expand All @@ -357,7 +357,7 @@ def build(options: Options, tmp_path: Path) -> None:
config,
dependency_constraint_flags,
build_options.environment,
build_frontend,
build_frontend.name,
)

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

extra_flags = split_config_settings(build_options.config_settings, build_frontend)
extra_flags = split_config_settings(
build_options.config_settings, build_frontend.name
)
extra_flags += build_frontend.args

if build_frontend == "pip":
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
Expand All @@ -395,7 +398,7 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags,
env=env,
)
elif build_frontend == "build":
elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
Expand Down
4 changes: 3 additions & 1 deletion cibuildwheel/oci_container.py
Expand Up @@ -29,7 +29,9 @@ class OCIContainerEngineConfig:

@staticmethod
def from_config_string(config_string: str) -> OCIContainerEngineConfig:
config_dict = parse_key_value_string(config_string, ["name"])
config_dict = parse_key_value_string(
config_string, ["name"], ["create_args", "create-args"]
)
name = " ".join(config_dict["name"])
if name not in {"docker", "podman"}:
msg = f"unknown container engine {name}"
Expand Down
28 changes: 15 additions & 13 deletions cibuildwheel/options.py
Expand Up @@ -27,7 +27,7 @@
from .util import (
MANYLINUX_ARCHS,
MUSLLINUX_ARCHS,
BuildFrontend,
BuildFrontendConfig,
BuildSelector,
DependencyConstraints,
TestSelector,
Expand Down Expand Up @@ -92,7 +92,7 @@ class BuildOptions:
test_requires: list[str]
test_extras: str
build_verbosity: int
build_frontend: BuildFrontend | Literal["default"]
build_frontend: BuildFrontendConfig | None
config_settings: str

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

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

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

try:
environment = parse_environment(environment_config)
Expand Down
49 changes: 38 additions & 11 deletions cibuildwheel/util.py
Expand Up @@ -57,16 +57,6 @@

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

BuildFrontend = Literal["pip", "build"]


def build_frontend_or_default(
setting: BuildFrontend | Literal["default"], default: BuildFrontend = "pip"
) -> BuildFrontend:
if setting == "default":
return default
return setting


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


BuildFrontendName = Literal["pip", "build"]


@dataclass(frozen=True)
class BuildFrontendConfig:
name: BuildFrontendName
args: Sequence[str] = ()

@staticmethod
def from_config_string(config_string: str) -> BuildFrontendConfig:
config_dict = parse_key_value_string(config_string, ["name"], ["args"])
name = " ".join(config_dict["name"])
if name not in {"pip", "build"}:
msg = f"Unrecognised build frontend {name}, only 'pip' and 'build' are supported"
raise ValueError(msg)

name = typing.cast(BuildFrontendName, name)

args = config_dict.get("args") or []
return BuildFrontendConfig(name=name, args=args)

def options_summary(self) -> str | dict[str, str]:
if not self.args:
return self.name
else:
return {"name": self.name, "args": repr(self.args)}


class NonPlatformWheelError(Exception):
def __init__(self) -> None:
message = textwrap.dedent(
Expand Down Expand Up @@ -699,13 +717,19 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:


def parse_key_value_string(
key_value_string: str, positional_arg_names: list[str] | None = None
key_value_string: str,
positional_arg_names: Sequence[str] | None = None,
kw_arg_names: Sequence[str] | None = None,
) -> dict[str, list[str]]:
"""
Parses a string like "docker; create_args: --some-option=value another-option"
"""
if positional_arg_names is None:
positional_arg_names = []
if kw_arg_names is None:
kw_arg_names = []

all_field_names = [*positional_arg_names, *kw_arg_names]

shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";:")
shlexer.commenters = ""
Expand All @@ -721,6 +745,9 @@ def parse_key_value_string(
if len(field) > 1 and field[1] == ":":
field_name = field[0]
values = field[2:]
if field_name not in all_field_names:
msg = f"Failed to parse {key_value_string!r}. Unknown field name {field_name!r}"
raise ValueError(msg)
else:
try:
field_name = positional_arg_names[field_i]
Expand Down
19 changes: 11 additions & 8 deletions cibuildwheel/windows.py
Expand Up @@ -25,10 +25,10 @@
from .util import (
CIBW_CACHE_PATH,
AlreadyBuiltWheelError,
BuildFrontend,
BuildFrontendConfig,
BuildFrontendName,
BuildSelector,
NonPlatformWheelError,
build_frontend_or_default,
call,
download,
find_compatible_wheel,
Expand Down Expand Up @@ -216,7 +216,7 @@ def setup_python(
python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment,
build_frontend: BuildFrontend,
build_frontend: BuildFrontendName,
) -> dict[str, str]:
tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0]
Expand Down Expand Up @@ -369,7 +369,7 @@ def build(options: Options, tmp_path: Path) -> None:

for config in python_configurations:
build_options = options.build_options(config.identifier)
build_frontend = build_frontend_or_default(build_options.build_frontend)
build_frontend = build_options.build_frontend or BuildFrontendConfig("pip")
log.build_start(config.identifier)

identifier_tmp_dir = tmp_path / config.identifier
Expand All @@ -390,7 +390,7 @@ def build(options: Options, tmp_path: Path) -> None:
config,
dependency_constraint_flags,
build_options.environment,
build_frontend,
build_frontend.name,
)

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

extra_flags = split_config_settings(build_options.config_settings, build_frontend)
extra_flags = split_config_settings(
build_options.config_settings, build_frontend.name
)
extra_flags += build_frontend.args

if build_frontend == "pip":
if build_frontend.name == "pip":
extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369
Expand All @@ -431,7 +434,7 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags,
env=env,
)
elif build_frontend == "build":
elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg)
Expand Down
18 changes: 17 additions & 1 deletion docs/options.md
Expand Up @@ -504,9 +504,19 @@ This option can also be set using the [command-line option](#command-line) `--pr
### `CIBW_BUILD_FRONTEND` {: #build-frontend}
> Set the tool to use to build, either "pip" (default for now) or "build"

Choose which build backend to use. Can either be "pip", which will run
Options:

- `pip[;args: ...]`
- `build[;args: ...]`

Default: `pip`

Choose which build frontend to use. Can either be "pip", which will run
`python -m pip wheel`, or "build", which will run `python -m build --wheel`.

You can specify extra arguments to pass to `pip wheel` or `build` using the
optional `args` option.

!!! tip
Until v2.0.0, [pip] was the only way to build wheels, and is still the
default. However, we expect that at some point in the future, cibuildwheel
Expand All @@ -526,6 +536,9 @@ Choose which build backend to use. Can either be "pip", which will run

# Ensure pip is used even if the default changes in the future
CIBW_BUILD_FRONTEND: "pip"

# supply an extra argument to 'pip wheel'
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
```

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

# Ensure pip is used even if the default changes in the future
build-frontend = "pip"

# supply an extra argument to 'pip wheel'
build-frontend = { name = "pip", args = ["--no-build-isolation"] }
```

### `CIBW_CONFIG_SETTINGS` {: #config-settings}
Expand Down
34 changes: 34 additions & 0 deletions test/test_build_frontend_args.py
@@ -0,0 +1,34 @@
import subprocess

import pytest

from . import utils
from .test_projects.c import new_c_project


@pytest.mark.parametrize("frontend_name", ["pip", "build"])
def test_build_frontend_args(tmp_path, capfd, frontend_name):
project = new_c_project()
project_dir = tmp_path / "project"
project.generate(project_dir)

# the build will fail because the frontend is called with '-h' - it prints the help message
with pytest.raises(subprocess.CalledProcessError):
utils.cibuildwheel_run(
project_dir,
add_env={
"CIBW_BUILD": "cp311-*",
"CIBW_BUILD_FRONTEND": f"{frontend_name}; args: -h",
},
)

captured = capfd.readouterr()
print(captured.out)

# check that the help message was printed
if frontend_name == "pip":
assert "Usage:" in captured.out
assert "Wheel Options:" in captured.out
else:
assert "usage:" in captured.out
assert "A simple, correct Python build frontend." in captured.out