Merge pull request #1588 from pypa/frontend-flags

Add the ability to pass extra flags to a build frontend through CIBW_BUILD_FRONTEND
This commit is contained in:
Joe Rickerby
2023-09-18 18:14:42 +01:00
committed by GitHub
10 changed files with 190 additions and 48 deletions
+6 -5
View File
@@ -16,9 +16,9 @@ from .options import Options
from .typing import PathOrStr from .typing import PathOrStr
from .util import ( from .util import (
AlreadyBuiltWheelError, AlreadyBuiltWheelError,
BuildFrontendConfig,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
build_frontend_or_default,
find_compatible_wheel, find_compatible_wheel,
get_build_verbosity_extra_flags, get_build_verbosity_extra_flags,
prepare_command, prepare_command,
@@ -177,7 +177,7 @@ def build_in_container(
for config in platform_configs: for config in platform_configs:
log.build_start(config.identifier) log.build_start(config.identifier)
build_options = options.build_options(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] = [] dependency_constraint_flags: list[PathOrStr] = []
@@ -243,9 +243,10 @@ def build_in_container(
container.call(["rm", "-rf", built_wheel_dir]) container.call(["rm", "-rf", built_wheel_dir])
container.call(["mkdir", "-p", 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) extra_flags += get_build_verbosity_extra_flags(build_options.build_verbosity)
container.call( container.call(
[ [
@@ -260,7 +261,7 @@ def build_in_container(
], ],
env=env, env=env,
) )
elif build_frontend == "build": elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
+11 -8
View File
@@ -25,10 +25,10 @@ from .typing import PathOrStr
from .util import ( from .util import (
CIBW_CACHE_PATH, CIBW_CACHE_PATH,
AlreadyBuiltWheelError, AlreadyBuiltWheelError,
BuildFrontend, BuildFrontendConfig,
BuildFrontendName,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
build_frontend_or_default,
call, call,
detect_ci_provider, detect_ci_provider,
download, download,
@@ -165,7 +165,7 @@ def setup_python(
python_configuration: PythonConfiguration, python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr], dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment, environment: ParsedEnvironment,
build_frontend: BuildFrontend, build_frontend: BuildFrontendName,
) -> dict[str, str]: ) -> dict[str, str]:
tmp.mkdir() tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0] implementation_id = python_configuration.identifier.split("-")[0]
@@ -334,7 +334,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(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")
log.build_start(config.identifier) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
@@ -357,7 +357,7 @@ def build(options: Options, tmp_path: Path) -> None:
config, config,
dependency_constraint_flags, dependency_constraint_flags,
build_options.environment, build_options.environment,
build_frontend, build_frontend.name,
) )
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
@@ -378,9 +378,12 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...") log.step("Building wheel...")
built_wheel_dir.mkdir() 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) 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 # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369 # see https://github.com/pypa/cibuildwheel/pull/369
@@ -395,7 +398,7 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=env, env=env,
) )
elif build_frontend == "build": elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
+3 -1
View File
@@ -29,7 +29,9 @@ class OCIContainerEngineConfig:
@staticmethod @staticmethod
def from_config_string(config_string: str) -> OCIContainerEngineConfig: 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"]) name = " ".join(config_dict["name"])
if name not in {"docker", "podman"}: if name not in {"docker", "podman"}:
msg = f"unknown container engine {name}" msg = f"unknown container engine {name}"
+14 -12
View File
@@ -27,7 +27,7 @@ from .typing import PLATFORMS, PlatformName
from .util import ( from .util import (
MANYLINUX_ARCHS, MANYLINUX_ARCHS,
MUSLLINUX_ARCHS, MUSLLINUX_ARCHS,
BuildFrontend, BuildFrontendConfig,
BuildSelector, BuildSelector,
DependencyConstraints, DependencyConstraints,
TestSelector, TestSelector,
@@ -92,7 +92,7 @@ class BuildOptions:
test_requires: list[str] test_requires: list[str]
test_extras: str test_extras: str
build_verbosity: int build_verbosity: int
build_frontend: BuildFrontend | Literal["default"] build_frontend: BuildFrontendConfig | None
config_settings: str config_settings: str
@property @property
@@ -488,7 +488,6 @@ class Options:
with self.reader.identifier(identifier): with self.reader.identifier(identifier):
before_all = self.reader.get("before-all", sep=" && ") 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_config = self.reader.get(
"environment", table={"item": '{k}="{v}"', "sep": " "} "environment", table={"item": '{k}="{v}"', "sep": " "}
) )
@@ -506,16 +505,19 @@ class Options:
test_extras = self.reader.get("test-extras", sep=",") test_extras = self.reader.get("test-extras", sep=",")
build_verbosity_str = self.reader.get("build-verbosity") build_verbosity_str = self.reader.get("build-verbosity")
build_frontend: BuildFrontend | Literal["default"] build_frontend_str = self.reader.get(
if build_frontend_str == "build": "build-frontend",
build_frontend = "build" env_plat=False,
elif build_frontend_str == "pip": table={"item": "{k}:{v}", "sep": "; ", "quote": shlex.quote},
build_frontend = "pip" )
elif build_frontend_str == "default": build_frontend: BuildFrontendConfig | None
build_frontend = "default" if not build_frontend_str or build_frontend_str == "default":
build_frontend = None
else: else:
msg = f"cibuildwheel: Unrecognised build frontend {build_frontend_str!r}, only 'pip' and 'build' are supported" try:
print(msg, file=sys.stderr) build_frontend = BuildFrontendConfig.from_config_string(build_frontend_str)
except ValueError as e:
print(f"cibuildwheel: {e}", file=sys.stderr)
sys.exit(2) sys.exit(2)
try: try:
+38 -11
View File
@@ -57,16 +57,6 @@ install_certifi_script: Final[Path] = resources_dir / "install_certifi.py"
test_fail_cwd_file: Final[Path] = resources_dir / "testing_temp_dir_file.py" 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, ...]] = ( MANYLINUX_ARCHS: Final[tuple[str, ...]] = (
"x86_64", "x86_64",
@@ -376,6 +366,34 @@ class DependencyConstraints:
return self.base_file_path.name 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): class NonPlatformWheelError(Exception):
def __init__(self) -> None: def __init__(self) -> None:
message = textwrap.dedent( message = textwrap.dedent(
@@ -699,13 +717,19 @@ def fix_ansi_codes_for_github_actions(text: str) -> str:
def parse_key_value_string( 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]]: ) -> dict[str, list[str]]:
""" """
Parses a string like "docker; create_args: --some-option=value another-option" Parses a string like "docker; create_args: --some-option=value another-option"
""" """
if positional_arg_names is None: if positional_arg_names is None:
positional_arg_names = [] 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 = shlex.shlex(key_value_string, posix=True, punctuation_chars=";:")
shlexer.commenters = "" shlexer.commenters = ""
@@ -721,6 +745,9 @@ def parse_key_value_string(
if len(field) > 1 and field[1] == ":": if len(field) > 1 and field[1] == ":":
field_name = field[0] field_name = field[0]
values = field[2:] 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: else:
try: try:
field_name = positional_arg_names[field_i] field_name = positional_arg_names[field_i]
+11 -8
View File
@@ -25,10 +25,10 @@ from .typing import PathOrStr
from .util import ( from .util import (
CIBW_CACHE_PATH, CIBW_CACHE_PATH,
AlreadyBuiltWheelError, AlreadyBuiltWheelError,
BuildFrontend, BuildFrontendConfig,
BuildFrontendName,
BuildSelector, BuildSelector,
NonPlatformWheelError, NonPlatformWheelError,
build_frontend_or_default,
call, call,
download, download,
find_compatible_wheel, find_compatible_wheel,
@@ -216,7 +216,7 @@ def setup_python(
python_configuration: PythonConfiguration, python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[PathOrStr], dependency_constraint_flags: Sequence[PathOrStr],
environment: ParsedEnvironment, environment: ParsedEnvironment,
build_frontend: BuildFrontend, build_frontend: BuildFrontendName,
) -> dict[str, str]: ) -> dict[str, str]:
tmp.mkdir() tmp.mkdir()
implementation_id = python_configuration.identifier.split("-")[0] implementation_id = python_configuration.identifier.split("-")[0]
@@ -369,7 +369,7 @@ def build(options: Options, tmp_path: Path) -> None:
for config in python_configurations: for config in python_configurations:
build_options = options.build_options(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")
log.build_start(config.identifier) log.build_start(config.identifier)
identifier_tmp_dir = tmp_path / config.identifier identifier_tmp_dir = tmp_path / config.identifier
@@ -390,7 +390,7 @@ def build(options: Options, tmp_path: Path) -> None:
config, config,
dependency_constraint_flags, dependency_constraint_flags,
build_options.environment, build_options.environment,
build_frontend, build_frontend.name,
) )
compatible_wheel = find_compatible_wheel(built_wheels, config.identifier) compatible_wheel = find_compatible_wheel(built_wheels, config.identifier)
@@ -414,9 +414,12 @@ def build(options: Options, tmp_path: Path) -> None:
log.step("Building wheel...") log.step("Building wheel...")
built_wheel_dir.mkdir() 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) 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 # Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/pypa/cibuildwheel/pull/369 # see https://github.com/pypa/cibuildwheel/pull/369
@@ -431,7 +434,7 @@ def build(options: Options, tmp_path: Path) -> None:
*extra_flags, *extra_flags,
env=env, env=env,
) )
elif build_frontend == "build": elif build_frontend.name == "build":
if not 0 <= build_options.build_verbosity < 2: if not 0 <= build_options.build_verbosity < 2:
msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring." msg = f"build_verbosity {build_options.build_verbosity} is not supported for build frontend. Ignoring."
log.warning(msg) log.warning(msg)
+17 -1
View File
@@ -504,9 +504,19 @@ This option can also be set using the [command-line option](#command-line) `--pr
### `CIBW_BUILD_FRONTEND` {: #build-frontend} ### `CIBW_BUILD_FRONTEND` {: #build-frontend}
> Set the tool to use to build, either "pip" (default for now) or "build" > 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`. `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 !!! tip
Until v2.0.0, [pip] was the only way to build wheels, and is still the 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 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
# Ensure pip is used even if the default changes in the future # Ensure pip is used even if the default changes in the future
CIBW_BUILD_FRONTEND: "pip" CIBW_BUILD_FRONTEND: "pip"
# supply an extra argument to 'pip wheel'
CIBW_BUILD_FRONTEND: "pip; args: --no-build-isolation"
``` ```
!!! tab examples "pyproject.toml" !!! tab examples "pyproject.toml"
@@ -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 # Ensure pip is used even if the default changes in the future
build-frontend = "pip" build-frontend = "pip"
# supply an extra argument to 'pip wheel'
build-frontend = { name = "pip", args = ["--no-build-isolation"] }
``` ```
### `CIBW_CONFIG_SETTINGS` {: #config-settings} ### `CIBW_CONFIG_SETTINGS` {: #config-settings}
+34
View File
@@ -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
+1 -1
View File
@@ -365,7 +365,7 @@ def test_defaults(platform, intercepted_build_args):
if isinstance(repair_wheel_default, list): if isinstance(repair_wheel_default, list):
repair_wheel_default = " && ".join(repair_wheel_default) repair_wheel_default = " && ".join(repair_wheel_default)
assert build_options.repair_command == repair_wheel_default assert build_options.repair_command == repair_wheel_default
assert build_options.build_frontend == defaults["build-frontend"] assert build_options.build_frontend is None
if platform == "linux": if platform == "linux":
assert build_options.manylinux_images assert build_options.manylinux_images
+54
View File
@@ -253,3 +253,57 @@ def test_container_engine_option(tmp_path: Path, toml_assignment, result_name, r
assert parsed_container_engine.name == result_name assert parsed_container_engine.name == result_name
assert parsed_container_engine.create_args == result_create_args assert parsed_container_engine.create_args == result_create_args
@pytest.mark.parametrize(
("toml_assignment", "result_name", "result_args"),
[
(
"",
None,
None,
),
(
'build-frontend = "build"',
"build",
[],
),
(
'build-frontend = {name = "build"}',
"build",
[],
),
(
'build-frontend = "pip; args: --some-option"',
"pip",
["--some-option"],
),
(
'build-frontend = {name = "pip", args = ["--some-option"]}',
"pip",
["--some-option"],
),
],
)
def test_build_frontend_option(tmp_path: Path, toml_assignment, result_name, result_args):
args = CommandLineArguments.defaults()
args.package_dir = tmp_path
tmp_path.joinpath("pyproject.toml").write_text(
textwrap.dedent(
f"""\
[tool.cibuildwheel]
{toml_assignment}
"""
)
)
options = Options(platform="linux", command_line_arguments=args, env={})
parsed_build_frontend = options.build_options(identifier=None).build_frontend
if toml_assignment:
assert parsed_build_frontend is not None
assert parsed_build_frontend.name == result_name
assert parsed_build_frontend.args == result_args
else:
assert parsed_build_frontend is None