Skip to content

Commit eb39da0

Browse files
henryiiijoerick
andauthored
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 <[email protected]> * feat: add --build * refactor: switch to using --only Signed-off-by: Henry Schreiner <[email protected]> * docs: cover --only Signed-off-by: Henry Schreiner <[email protected]> * docs: undo change to --plat for auditwheel Signed-off-by: Henry Schreiner <[email protected]> * (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 <[email protected]> Signed-off-by: Henry Schreiner <[email protected]> Co-authored-by: Joe Rickerby <[email protected]>
1 parent 3a46bde commit eb39da0

File tree

8 files changed

+156
-27
lines changed

8 files changed

+156
-27
lines changed

action.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ inputs:
1313
description: 'File containing the config, defaults to {package}/pyproject.toml'
1414
required: false
1515
default: ''
16+
only:
17+
description: 'Build a specific wheel only. No need for arch/platform if this is set'
18+
required: false
19+
default: ''
1620
branding:
1721
icon: package
1822
color: yellow
@@ -36,5 +40,6 @@ runs:
3640
${{ inputs.package-dir }}
3741
--output-dir ${{ inputs.output-dir }}
3842
--config-file "${{ inputs.config-file }}"
43+
--only "${{ inputs.only }}"
3944
2>&1
4045
shell: bash

cibuildwheel/__main__.py

Lines changed: 44 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import sys
77
import tarfile
88
import textwrap
9+
import typing
910
from pathlib import Path
1011
from tempfile import mkdtemp
1112

@@ -40,7 +41,7 @@ def main() -> None:
4041
parser.add_argument(
4142
"--platform",
4243
choices=["auto", "linux", "macos", "windows"],
43-
default=os.environ.get("CIBW_PLATFORM", "auto"),
44+
default=None,
4445
help="""
4546
Platform to build for. Use this option to override the
4647
auto-detected platform or to run cibuildwheel on your development
@@ -64,6 +65,16 @@ def main() -> None:
6465
""",
6566
)
6667

68+
parser.add_argument(
69+
"--only",
70+
default=None,
71+
help="""
72+
Force a single wheel build when given an identifier. Overrides
73+
CIBW_BUILD/CIBW_SKIP. --platform and --arch cannot be specified
74+
if this is given.
75+
""",
76+
)
77+
6778
parser.add_argument(
6879
"--output-dir",
6980
type=Path,
@@ -154,10 +165,40 @@ def main() -> None:
154165

155166

156167
def build_in_directory(args: CommandLineArguments) -> None:
168+
platform_option_value = args.platform or os.environ.get("CIBW_PLATFORM", "auto")
157169
platform: PlatformName
158170

159-
if args.platform != "auto":
160-
platform = args.platform
171+
if args.only:
172+
if "linux_" in args.only:
173+
platform = "linux"
174+
elif "macosx_" in args.only:
175+
platform = "macos"
176+
elif "win_" in args.only:
177+
platform = "windows"
178+
else:
179+
print(
180+
f"Invalid --only='{args.only}', must be a build selector with a known platform",
181+
file=sys.stderr,
182+
)
183+
sys.exit(2)
184+
if args.platform is not None:
185+
print(
186+
"--platform cannot be specified with --only, it is computed from --only",
187+
file=sys.stderr,
188+
)
189+
sys.exit(2)
190+
if args.archs is not None:
191+
print(
192+
"--arch cannot be specified with --only, it is computed from --only",
193+
file=sys.stderr,
194+
)
195+
sys.exit(2)
196+
elif platform_option_value != "auto":
197+
if platform_option_value not in PLATFORMS:
198+
print(f"cibuildwheel: Unsupported platform: {platform_option_value}", file=sys.stderr)
199+
sys.exit(2)
200+
201+
platform = typing.cast(PlatformName, platform_option_value)
161202
else:
162203
ci_provider = detect_ci_provider()
163204
if ci_provider is None:
@@ -187,10 +228,6 @@ def build_in_directory(args: CommandLineArguments) -> None:
187228
)
188229
sys.exit(2)
189230

190-
if platform not in PLATFORMS:
191-
print(f"cibuildwheel: Unsupported platform: {platform}", file=sys.stderr)
192-
sys.exit(2)
193-
194231
options = compute_options(platform=platform, command_line_arguments=args)
195232

196233
package_dir = options.globals.package_dir

cibuildwheel/options.py

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@
4242

4343
@dataclass
4444
class CommandLineArguments:
45-
platform: Literal["auto", "linux", "macos", "windows"]
45+
platform: Literal["auto", "linux", "macos", "windows"] | None
4646
archs: str | None
4747
output_dir: Path
48+
only: str | None
4849
config_file: str
4950
package_dir: Path
5051
print_build_identifiers: bool
@@ -403,6 +404,15 @@ def globals(self) -> GlobalOptions:
403404
)
404405
requires_python = None if requires_python_str is None else SpecifierSet(requires_python_str)
405406

407+
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
408+
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
409+
410+
# Process `--only`
411+
if args.only:
412+
build_config = args.only
413+
skip_config = ""
414+
architectures = Architecture.all_archs(self.platform)
415+
406416
build_selector = BuildSelector(
407417
build_config=build_config,
408418
skip_config=skip_config,
@@ -411,9 +421,6 @@ def globals(self) -> GlobalOptions:
411421
)
412422
test_selector = TestSelector(skip_config=test_skip)
413423

414-
archs_config_str = args.archs or self.reader.get("archs", sep=" ")
415-
architectures = Architecture.parse_config(archs_config_str, platform=self.platform)
416-
417424
container_engine_str = self.reader.get("container-engine")
418425

419426
if container_engine_str not in ["docker", "podman"]:
@@ -588,6 +595,9 @@ def summary(self, identifiers: list[str]) -> str:
588595
]
589596

590597
build_option_defaults = self.build_options(identifier=None)
598+
build_options_for_identifier = {
599+
identifier: self.build_options(identifier) for identifier in identifiers
600+
}
591601

592602
for option_name, default_value in sorted(asdict(build_option_defaults).items()):
593603
if option_name == "globals":
@@ -597,7 +607,7 @@ def summary(self, identifiers: list[str]) -> str:
597607

598608
# if any identifiers have an overridden value, print that too
599609
for identifier in identifiers:
600-
option_value = getattr(self.build_options(identifier=identifier), option_name)
610+
option_value = getattr(build_options_for_identifier[identifier], option_name)
601611
if option_value != default_value:
602612
lines.append(f" {identifier}: {option_value!r}")
603613

cibuildwheel/util.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def shell(*commands: str, env: dict[str, str] | None = None, cwd: PathOrStr | No
145145
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
146146

147147

148-
def format_safe(template: str, **kwargs: Any) -> str:
148+
def format_safe(template: str, **kwargs: str | os.PathLike[str]) -> str:
149149
"""
150150
Works similarly to `template.format(**kwargs)`, except that unmatched
151151
fields in `template` are passed through untouched.
@@ -173,11 +173,9 @@ def format_safe(template: str, **kwargs: Any) -> str:
173173
re.VERBOSE,
174174
)
175175

176-
# we use a function for repl to prevent re.sub interpreting backslashes
177-
# in repl as escape sequences.
178176
result = re.sub(
179177
pattern=find_pattern,
180-
repl=lambda _: str(value), # pylint: disable=cell-var-from-loop
178+
repl=str(value).replace("\\", r"\\"),
181179
string=result,
182180
)
183181

docs/options.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ The complete set of defaults for the current version of cibuildwheel are shown b
113113

114114
!!! tip
115115
Static configuration works across all CI systems, and can be used locally if
116-
you run `cibuildwheel --plat linux`. This is preferred, but environment
116+
you run `cibuildwheel --platform linux`. This is preferred, but environment
117117
variables are better if you need to change per-matrix element
118118
(`CIBW_BUILD` is often in this category, for example), or if you cannot or do
119119
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
202202

203203
This is even more convenient if you store your cibuildwheel config in [`pyproject.toml`](#configuration-file).
204204

205+
You can also run a single identifier with `--only <identifier>`. This will
206+
not require `--platform` or `--arch`, and will override any build/skip
207+
configuration.
208+
205209
### `CIBW_BUILD`, `CIBW_SKIP` {: #build-skip}
206210

207211
> Choose the Python versions to build

unit_test/main_tests/conftest.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,13 @@
1212

1313

1414
class ArgsInterceptor:
15+
def __init__(self):
16+
self.call_count = 0
17+
self.args = None
18+
self.kwargs = None
19+
1520
def __call__(self, *args, **kwargs):
21+
self.call_count += 1
1622
self.args = args
1723
self.kwargs = kwargs
1824

@@ -75,16 +81,14 @@ def platform(request, monkeypatch):
7581

7682

7783
@pytest.fixture
78-
def intercepted_build_args(platform, monkeypatch):
84+
def intercepted_build_args(monkeypatch):
7985
intercepted = ArgsInterceptor()
8086

81-
if platform == "linux":
82-
monkeypatch.setattr(linux, "build", intercepted)
83-
elif platform == "macos":
84-
monkeypatch.setattr(macos, "build", intercepted)
85-
elif platform == "windows":
86-
monkeypatch.setattr(windows, "build", intercepted)
87-
else:
88-
raise ValueError(f"unknown platform value: {platform}")
87+
monkeypatch.setattr(linux, "build", intercepted)
88+
monkeypatch.setattr(macos, "build", intercepted)
89+
monkeypatch.setattr(windows, "build", intercepted)
90+
91+
yield intercepted
8992

90-
return intercepted
93+
# check that intercepted_build_args only ever had one set of args
94+
assert intercepted.call_count <= 1

unit_test/main_tests/main_platform_test.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,73 @@ def test_archs_platform_all(platform, intercepted_build_args, monkeypatch):
192192
Architecture.arm64,
193193
Architecture.universal2,
194194
}
195+
196+
197+
@pytest.mark.parametrize(
198+
"only,plat",
199+
(
200+
("cp311-manylinux_x86_64", "linux"),
201+
("cp310-win_amd64", "windows"),
202+
("cp311-macosx_x86_64", "macos"),
203+
),
204+
)
205+
def test_only_argument(intercepted_build_args, monkeypatch, only, plat):
206+
monkeypatch.setenv("CIBW_BUILD", "unused")
207+
monkeypatch.setenv("CIBW_SKIP", "unused")
208+
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", only])
209+
210+
main()
211+
212+
options = intercepted_build_args.args[0]
213+
assert options.globals.build_selector.build_config == only
214+
assert options.globals.build_selector.skip_config == ""
215+
assert options.platform == plat
216+
assert options.globals.architectures == Architecture.all_archs(plat)
217+
218+
219+
@pytest.mark.parametrize("only", ("cp311-manylxinux_x86_64", "some_linux_thing"))
220+
def test_only_failed(monkeypatch, only):
221+
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", only])
222+
223+
with pytest.raises(SystemExit):
224+
main()
225+
226+
227+
def test_only_no_platform(monkeypatch):
228+
monkeypatch.setattr(
229+
sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64", "--platform", "macos"]
230+
)
231+
232+
with pytest.raises(SystemExit):
233+
main()
234+
235+
236+
def test_only_no_archs(monkeypatch):
237+
monkeypatch.setattr(
238+
sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64", "--archs", "x86_64"]
239+
)
240+
241+
with pytest.raises(SystemExit):
242+
main()
243+
244+
245+
@pytest.mark.parametrize(
246+
"envvar_name,envvar_value",
247+
(
248+
("CIBW_BUILD", "cp310-*"),
249+
("CIBW_SKIP", "cp311-*"),
250+
("CIBW_ARCHS", "auto32"),
251+
("CIBW_PLATFORM", "macos"),
252+
),
253+
)
254+
def test_only_overrides_env_vars(monkeypatch, intercepted_build_args, envvar_name, envvar_value):
255+
monkeypatch.setattr(sys, "argv", sys.argv + ["--only", "cp311-manylinux_x86_64"])
256+
monkeypatch.setenv(envvar_name, envvar_value)
257+
258+
main()
259+
260+
options = intercepted_build_args.args[0]
261+
assert options.globals.build_selector.build_config == "cp311-manylinux_x86_64"
262+
assert options.globals.build_selector.skip_config == ""
263+
assert options.platform == "linux"
264+
assert options.globals.architectures == Architecture.all_archs("linux")

unit_test/utils.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ def get_default_command_line_arguments() -> CommandLineArguments:
1010
platform="auto",
1111
allow_empty=False,
1212
archs=None,
13+
only=None,
1314
config_file="",
1415
output_dir=Path("wheelhouse"),
1516
package_dir=Path("."),

0 commit comments

Comments
 (0)