feat: add --only (#1098)

* fix(style): use sub recommended replacement method

See https://docs.python.org/3/library/re.html#re.escape

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* feat: add --build

* refactor: switch to using --only

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* docs: cover --only

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* docs: undo change to --plat for auditwheel

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

* (unrelated) speed up running the unit tests

based on a profile run, this speeds up the unit tests by 3x

* Remove `intercepted_build_args` reliance on `platform` fixture

* Add test that checks if 'only' overrides env var options

* Ensure that the --only command line option overrides env vars

* Fix UnboundLocalError

* Test cleanups

* style: typing.cast instead of plain cast

* feat: add only: to the GHA

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>

Signed-off-by: Henry Schreiner <henryschreineriii@gmail.com>
Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
Henry Schreiner
2022-09-09 08:34:47 -04:00
committed by GitHub
co-authored by Joe Rickerby
parent 3a46bde3fb
commit eb39da0b10
8 changed files with 156 additions and 27 deletions
+5
View File
@@ -13,6 +13,10 @@ inputs:
description: 'File containing the config, defaults to {package}/pyproject.toml' description: 'File containing the config, defaults to {package}/pyproject.toml'
required: false required: false
default: '' default: ''
only:
description: 'Build a specific wheel only. No need for arch/platform if this is set'
required: false
default: ''
branding: branding:
icon: package icon: package
color: yellow color: yellow
@@ -36,5 +40,6 @@ runs:
${{ inputs.package-dir }} ${{ inputs.package-dir }}
--output-dir ${{ inputs.output-dir }} --output-dir ${{ inputs.output-dir }}
--config-file "${{ inputs.config-file }}" --config-file "${{ inputs.config-file }}"
--only "${{ inputs.only }}"
2>&1 2>&1
shell: bash shell: bash
+44 -7
View File
@@ -6,6 +6,7 @@ import shutil
import sys import sys
import tarfile import tarfile
import textwrap import textwrap
import typing
from pathlib import Path from pathlib import Path
from tempfile import mkdtemp from tempfile import mkdtemp
@@ -40,7 +41,7 @@ def main() -> None:
parser.add_argument( parser.add_argument(
"--platform", "--platform",
choices=["auto", "linux", "macos", "windows"], choices=["auto", "linux", "macos", "windows"],
default=os.environ.get("CIBW_PLATFORM", "auto"), default=None,
help=""" help="""
Platform to build for. Use this option to override the Platform to build for. Use this option to override the
auto-detected platform or to run cibuildwheel on your development auto-detected platform or to run cibuildwheel on your development
@@ -64,6 +65,16 @@ def main() -> None:
""", """,
) )
parser.add_argument(
"--only",
default=None,
help="""
Force a single wheel build when given an identifier. Overrides
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
if this is given.
""",
)
parser.add_argument( parser.add_argument(
"--output-dir", "--output-dir",
type=Path, type=Path,
@@ -154,10 +165,40 @@ def main() -> None:
def build_in_directory(args: CommandLineArguments) -> None: def build_in_directory(args: CommandLineArguments) -> None:
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto")
platform: PlatformName platform: PlatformName
if args.platform != "auto": if args.only:
platform = args.platform if "linux_" in args.only:
platform = "linux"
elif "macosx_" in args.only:
platform = "macos"
elif "win_" in args.only:
platform = "windows"
else:
print(
f"Invalid --only='{args.only}', must be a build selector with a known platform",
file=sys.stderr,
)
sys.exit(2)
if args.platform is not None:
print(
"--platform cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
if args.archs is not None:
print(
"--arch cannot be specified with --only, it is computed from --only",
file=sys.stderr,
)
sys.exit(2)
elif platform_option_value != "auto":
if platform_option_value not in PLATFORMS:
print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
sys.exit(2)
platform = typing.cast(PlatformName, platform_option_value)
else: else:
ci_provider = detect_ci_provider() ci_provider = detect_ci_provider()
if ci_provider is None: if ci_provider is None:
@@ -187,10 +228,6 @@ def build_in_directory(args: CommandLineArguments) -> None:
) )
sys.exit(2) sys.exit(2)
if platform not in PLATFORMS:
print(f"cibuildwheel: Unsupported platform: {platform}", file=sys.stderr)
sys.exit(2)
options = compute_options(platform=platform, command_line_arguments=args) options = compute_options(platform=platform, command_line_arguments=args)
package_dir = options.globals.package_dir package_dir = options.globals.package_dir
+15 -5
View File
@@ -42,9 +42,10 @@ from .util import (
@dataclass @dataclass
class CommandLineArguments: class CommandLineArguments:
platform: Literal["auto", "linux", "macos", "windows"] platform: Literal["auto", "linux", "macos", "windows"] | None
archs: str | None archs: str | None
output_dir: Path output_dir: Path
only: str | None
config_file: str config_file: str
package_dir: Path package_dir: Path
print_build_identifiers: bool print_build_identifiers: bool
@@ -403,6 +404,15 @@ class Options:
) )
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str) requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
# Process `--only`
if args.only:
build_config = args.only
skip_config = ""
architectures = Architecture.all_archs(self.platform)
build_selector = BuildSelector( build_selector = BuildSelector(
build_config=build_config, build_config=build_config,
skip_config=skip_config, skip_config=skip_config,
@@ -411,9 +421,6 @@ class Options:
) )
test_selector = TestSelector(skip_config=test_skip) test_selector = TestSelector(skip_config=test_skip)
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
container_engine_str = self.reader.get("container-engine") container_engine_str = self.reader.get("container-engine")
if container_engine_str not in ["docker", "podman"]: if container_engine_str not in ["docker", "podman"]:
@@ -588,6 +595,9 @@ class Options:
] ]
build_option_defaults = self.build_options(identifier=None) build_option_defaults = self.build_options(identifier=None)
build_options_for_identifier = {
identifier: self.build_options(identifier) for identifier in identifiers
}
for option_name, default_value in sorted(asdict(build_option_defaults).items()): for option_name, default_value in sorted(asdict(build_option_defaults).items()):
if option_name == "globals": if option_name == "globals":
@@ -597,7 +607,7 @@ class Options:
# if any identifiers have an overridden value, print that too # if any identifiers have an overridden value, print that too
for identifier in identifiers: for identifier in identifiers:
option_value = getattr(self.build_options(identifier=identifier), option_name) option_value = getattr(build_options_for_identifier[identifier], option_name)
if option_value != default_value: if option_value != default_value:
lines.append(f" {identifier}: {option_value!r}") lines.append(f" {identifier}: {option_value!r}")
+2 -4
View File
@@ -145,7 +145,7 @@ def shell(*commands: str, env: dict[str, str] | None = None, cwd: PathOrStr | No
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True) subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
def format_safe(template: str, **kwargs: Any) -> str: def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
""" """
Works similarly to `template.format(**kwargs)`, except that unmatched Works similarly to `template.format(**kwargs)`, except that unmatched
fields in `template` are passed through untouched. fields in `template` are passed through untouched.
@@ -173,11 +173,9 @@ def format_safe(template: str, **kwargs: Any) -> str:
re.VERBOSE, re.VERBOSE,
) )
# we use a function for repl to prevent re.sub interpreting backslashes
# in repl as escape sequences.
result = re.sub( result = re.sub(
pattern=find_pattern, pattern=find_pattern,
repl=lambda _: str(value), # pylint: disable=cell-var-from-loop repl=str(value).replace("\\", r"\\"),
string=result, string=result,
) )
+5 -1
View File
@@ -113,7 +113,7 @@ The complete set of defaults for the current version of cibuildwheel are shown b
!!! tip !!! tip
Static configuration works across all CI systems, and can be used locally if Static configuration works across all CI systems, and can be used locally if
you run `cibuildwheel --plat linux`. This is preferred, but environment you run `cibuildwheel --platform linux`. This is preferred, but environment
variables are better if you need to change per-matrix element variables are better if you need to change per-matrix element
(`CIBW_BUILD` is often in this category, for example), or if you cannot or do (`CIBW_BUILD` is often in this category, for example), or if you cannot or do
not want to change a `pyproject.toml` file. You can specify a different file to not want to change a `pyproject.toml` file. You can specify a different file to
@@ -202,6 +202,10 @@ This option can also be set using the [command-line option](#command-line) `--pl
This is even more convenient if you store your cibuildwheel config in [`pyproject.toml`](#configuration-file). This is even more convenient if you store your cibuildwheel config in [`pyproject.toml`](#configuration-file).
You can also run a single identifier with `--only <identifier>`. This will
not require `--platform` or `--arch`, and will override any build/skip
configuration.
### `CIBW_BUILD`, `CIBW_SKIP` {: #build-skip} ### `CIBW_BUILD`, `CIBW_SKIP` {: #build-skip}
> Choose the Python versions to build > Choose the Python versions to build
+14 -10
View File
@@ -12,7 +12,13 @@ from cibuildwheel import linux, macos, util, windows
class ArgsInterceptor: class ArgsInterceptor:
def __init__(self):
self.call_count = 0
self.args = None
self.kwargs = None
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
self.call_count += 1
self.args = args self.args = args
self.kwargs = kwargs self.kwargs = kwargs
@@ -75,16 +81,14 @@ def platform(request, monkeypatch):
@pytest.fixture @pytest.fixture
def intercepted_build_args(platform, monkeypatch): def intercepted_build_args(monkeypatch):
intercepted = ArgsInterceptor() intercepted = ArgsInterceptor()
if platform == "linux": monkeypatch.setattr(linux, "build", intercepted)
monkeypatch.setattr(linux, "build", intercepted) monkeypatch.setattr(macos, "build", intercepted)
elif platform == "macos": monkeypatch.setattr(windows, "build", intercepted)
monkeypatch.setattr(macos, "build", intercepted)
elif platform == "windows":
monkeypatch.setattr(windows, "build", intercepted)
else:
raise ValueError(f"unknown platform value: {platform}")
return intercepted yield intercepted
# check that intercepted_build_args only ever had one set of args
assert intercepted.call_count <= 1
@@ -192,3 +192,73 @@ def test_archs_platform_all(platform, intercepted_build_args, monkeypatch):
Architecture.arm64, Architecture.arm64,
Architecture.universal2, Architecture.universal2,
} }
@pytest.mark.parametrize(
"only,plat",
(
("cp311-manylinux_x86_64", "linux"),
("cp310-win_amd64", "windows"),
("cp311-macosx_x86_64", "macos"),
),
)
def test_only_argument(intercepted_build_args, monkeypatch, only, plat):
monkeypatch.setenv("CIBW_BUILD", "unused")
monkeypatch.setenv("CIBW_SKIP", "unused")
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", only])
main()
options = intercepted_build_args.args[0]
assert options.globals.build_selector.build_config == only
assert options.globals.build_selector.skip_config == ""
assert options.platform == plat
assert options.globals.architectures == Architecture.all_archs(plat)
@pytest.mark.parametrize("only", ("cp311-manylxinux_x86_64", "some_linux_thing"))
def test_only_failed(monkeypatch, only):
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", only])
with pytest.raises(SystemExit):
main()
def test_only_no_platform(monkeypatch):
monkeypatch.setattr(
sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64", "--platform", "macos"]
)
with pytest.raises(SystemExit):
main()
def test_only_no_archs(monkeypatch):
monkeypatch.setattr(
sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64", "--archs", "x86_64"]
)
with pytest.raises(SystemExit):
main()
@pytest.mark.parametrize(
"envvar_name,envvar_value",
(
("CIBW_BUILD", "cp310-*"),
("CIBW_SKIP", "cp311-*"),
("CIBW_ARCHS", "auto32"),
("CIBW_PLATFORM", "macos"),
),
)
def test_only_overrides_env_vars(monkeypatch, intercepted_build_args, envvar_name, envvar_value):
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64"])
monkeypatch.setenv(envvar_name, envvar_value)
main()
options = intercepted_build_args.args[0]
assert options.globals.build_selector.build_config == "cp311-manylinux_x86_64"
assert options.globals.build_selector.skip_config == ""
assert options.platform == "linux"
assert options.globals.architectures == Architecture.all_archs("linux")
+1
View File
@@ -10,6 +10,7 @@ def get_default_command_line_arguments() -> CommandLineArguments:
platform="auto", platform="auto",
allow_empty=False, allow_empty=False,
archs=None, archs=None,
only=None,
config_file="", config_file="",
output_dir=Path("wheelhouse"), output_dir=Path("wheelhouse"),
package_dir=Path("."), package_dir=Path("."),