From ffd2e1824079a17aa5c53b5ec05c593abc628f8e Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Sun, 2 Aug 2026 07:49:12 -0400 Subject: [PATCH] fix(schema): default is fine for pyodide too (#2951) --- .gitignore | 1 + bin/generate_schema.py | 95 +++++++++++-------- .../resources/cibuildwheel.schema.json | 82 ++++++++++++++-- unit_test/validate_schema_test.py | 59 ++++++++++++ 4 files changed, 185 insertions(+), 52 deletions(-) diff --git a/.gitignore b/.gitignore index d06a56b8..4070ff8e 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ site/ # This file should be a symlink or contain "See @AGENTS.md" CLAUDE.md +.claude/ diff --git a/bin/generate_schema.py b/bin/generate_schema.py index 46d39e4d..4c127d18 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -35,7 +35,7 @@ $defs: - pyodide-prerelease - pypy - pypy-eol - description: A Python version or flavor to enable. + description: A Python version or flavor to enable. additionalProperties: false description: cibuildwheel's settings. type: object @@ -62,29 +62,7 @@ properties: default: ['*'] description: Choose the Python versions to build. type: string_array - build-frontend: - default: default - description: Set the tool to use to build, either "build" (default), "build[uv]", "uv", or "pip" - oneOf: - - enum: [pip, build, "build[uv]", uv, default] - - type: string - pattern: '^pip; ?args:' - - type: string - pattern: '^build; ?args:' - - type: string - pattern: '^build\\[uv\\]; ?args:' - - type: string - pattern: '^uv; ?args:' - - type: object - additionalProperties: false - required: [name] - properties: - name: - enum: [pip, build, "build[uv]", uv] - args: - type: array - items: - type: string + build-frontend: {} # filled in by build_frontend_schema below build-verbosity: type: integer minimum: -3 @@ -292,6 +270,43 @@ string_table = yaml.safe_load( """ ) +FRONTENDS = ["pip", "build", "build[uv]", "uv"] + + +def build_frontend_schema( + names: list[str], description: str, default: str = "default" +) -> dict[str, Any]: + """ + A frontend is a name, a "name; args: ..." string, or a table with a name and args. + """ + # Only the brackets in "build[uv]" need escaping + patterns = [name.replace("[", r"\[").replace("]", r"\]") for name in names] + return { + "default": default, + "description": description, + "oneOf": [ + {"enum": [*names, "default"]}, + *({"type": "string", "pattern": f"^{pattern}; ?args:"} for pattern in patterns), + { + "type": "object", + "additionalProperties": False, + "required": ["name"], + "properties": { + "name": {"enum": names}, + "args": {"type": "array", "items": {"type": "string"}}, + }, + }, + ], + "title": "CIBW_BUILD_FRONTEND", + } + + +schema["properties"]["build-frontend"] = build_frontend_schema( + [*FRONTENDS, "pyodide-build"], + 'Set the tool to use to build, either "build" (default), "build[uv]", "uv", or "pip"' + ' ("pyodide-build" for pyodide)', +) + for value in schema["properties"].values(): match value: case {"type": "string_array"}: @@ -393,24 +408,20 @@ for os_name, command in [ del oses["linux"]["properties"]["dependency-versions"] -oses["pyodide"]["properties"]["build-frontend"] = { - **schema["properties"]["build-frontend"], - "default": "pyodide-build", - "description": 'On the pyodide platform, the build frontend must be "pyodide-build"', - "oneOf": [ - {"enum": ["pyodide-build"]}, - {"type": "string", "pattern": "^pyodide-build; ?args:"}, - { - "type": "object", - "additionalProperties": False, - "required": ["name"], - "properties": { - "name": {"enum": ["pyodide-build"]}, - "args": {"type": "array", "items": {"type": "string"}}, - }, - }, - ], -} + +schema["$defs"]["build-frontend-no-pyodide"] = build_frontend_schema( + FRONTENDS, + 'Set the tool to use to build, either "build" (default), "build[uv]", "uv", or "pip"', +) + +for os_val in oses.values(): + os_val["properties"]["build-frontend"] = {"$ref": "#/$defs/build-frontend-no-pyodide"} + +oses["pyodide"]["properties"]["build-frontend"] = build_frontend_schema( + ["pyodide-build"], + 'On the pyodide platform, the build frontend must be "pyodide-build"', + default="pyodide-build", +) schema["properties"]["overrides"] = overrides schema["properties"] |= oses diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 216461f8..afb2cece 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -18,9 +18,64 @@ "pyodide-prerelease", "pypy", "pypy-eol" - ] + ], + "description": "A Python version or flavor to enable." }, - "description": "A Python version or flavor to enable." + "build-frontend-no-pyodide": { + "default": "default", + "description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", \"uv\", or \"pip\"", + "oneOf": [ + { + "enum": [ + "pip", + "build", + "build[uv]", + "uv", + "default" + ] + }, + { + "type": "string", + "pattern": "^pip; ?args:" + }, + { + "type": "string", + "pattern": "^build; ?args:" + }, + { + "type": "string", + "pattern": "^build\\[uv\\]; ?args:" + }, + { + "type": "string", + "pattern": "^uv; ?args:" + }, + { + "type": "object", + "additionalProperties": false, + "required": [ + "name" + ], + "properties": { + "name": { + "enum": [ + "pip", + "build", + "build[uv]", + "uv" + ] + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + ], + "title": "CIBW_BUILD_FRONTEND" + } }, "additionalProperties": false, "description": "cibuildwheel's settings.", @@ -136,7 +191,7 @@ }, "build-frontend": { "default": "default", - "description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", \"uv\", or \"pip\"", + "description": "Set the tool to use to build, either \"build\" (default), \"build[uv]\", \"uv\", or \"pip\" (\"pyodide-build\" for pyodide)", "oneOf": [ { "enum": [ @@ -144,6 +199,7 @@ "build", "build[uv]", "uv", + "pyodide-build", "default" ] }, @@ -163,6 +219,10 @@ "type": "string", "pattern": "^uv; ?args:" }, + { + "type": "string", + "pattern": "^pyodide-build; ?args:" + }, { "type": "object", "additionalProperties": false, @@ -175,7 +235,8 @@ "pip", "build", "build[uv]", - "uv" + "uv", + "pyodide-build" ] }, "args": { @@ -894,7 +955,7 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "$ref": "#/$defs/build-frontend-no-pyodide" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" @@ -1033,7 +1094,7 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "$ref": "#/$defs/build-frontend-no-pyodide" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" @@ -1118,7 +1179,7 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "$ref": "#/$defs/build-frontend-no-pyodide" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" @@ -1208,7 +1269,8 @@ "oneOf": [ { "enum": [ - "pyodide-build" + "pyodide-build", + "default" ] }, { @@ -1308,7 +1370,7 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "$ref": "#/$defs/build-frontend-no-pyodide" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" @@ -1393,7 +1455,7 @@ "$ref": "#/properties/before-test" }, "build-frontend": { - "$ref": "#/properties/build-frontend" + "$ref": "#/$defs/build-frontend-no-pyodide" }, "build-verbosity": { "$ref": "#/properties/build-verbosity" diff --git a/unit_test/validate_schema_test.py b/unit_test/validate_schema_test.py index 85f5eae6..1ba5c0e8 100644 --- a/unit_test/validate_schema_test.py +++ b/unit_test/validate_schema_test.py @@ -143,6 +143,65 @@ def test_overrides_invalid_inherit_value(validator: validate_pyproject.api.Valid validator(example) +def test_pyodide_build_frontend(validator: validate_pyproject.api.Validator) -> None: + """ + pyodide-build is accepted anywhere the frontend can be set, as an override + or a global value can apply to pyodide builds only. + """ + example = tomllib.loads( + """ + [tool.cibuildwheel] + build-frontend = "pyodide-build" + + [tool.cibuildwheel.pyodide] + build-frontend = { name = "pyodide-build", args = ["--some-arg"] } + + [[tool.cibuildwheel.overrides]] + select = "*pyodide*" + build-frontend = "pyodide-build; args: --some-arg" + """ + ) + + assert validator(example) is not None + + +@pytest.mark.parametrize("frontend", ['"uv"', '"build; args: --some-arg"', '{ name = "pip" }']) +def test_pyodide_bad_build_frontend( + validator: validate_pyproject.api.Validator, frontend: str +) -> None: + """ + The pyodide table only accepts the pyodide-build frontend. + """ + example = tomllib.loads( + f""" + [tool.cibuildwheel.pyodide] + build-frontend = {frontend} + """ + ) + + with pytest.raises(validate_pyproject.error_reporting.ValidationError): + validator(example) + + +@pytest.mark.parametrize("platform", ["linux", "macos", "windows", "ios", "android"]) +@pytest.mark.parametrize("frontend", ["pyodide-build", "pyodide-build; args: --some-arg"]) +def test_bad_pyodide_build_frontend( + validator: validate_pyproject.api.Validator, platform: str, frontend: str +) -> None: + """ + Only the pyodide table accepts the pyodide-build frontend. + """ + example = tomllib.loads( + f""" + [tool.cibuildwheel.{platform}] + build-frontend = "{frontend}" + """ + ) + + with pytest.raises(validate_pyproject.error_reporting.ValidationError): + validator(example) + + def test_docs_examples(validator: validate_pyproject.api.Validator) -> None: """ Parse out all the configuration examples, build valid TOML out of them, and