diff --git a/bin/generate_schema.py b/bin/generate_schema.py index e98f41b8b..0a357be9a 100755 --- a/bin/generate_schema.py +++ b/bin/generate_schema.py @@ -43,25 +43,8 @@ inherit: type: object additionalProperties: false - properties: - audit-command: {"$ref": "#/$defs/inherit"} - audit-requires: {"$ref": "#/$defs/inherit"} - before-all: {"$ref": "#/$defs/inherit"} - before-build: {"$ref": "#/$defs/inherit"} - xbuild-tools: {"$ref": "#/$defs/inherit"} - xbuild-files: {"$ref": "#/$defs/inherit"} - before-test: {"$ref": "#/$defs/inherit"} - config-settings: {"$ref": "#/$defs/inherit"} - container-engine: {"$ref": "#/$defs/inherit"} - environment: {"$ref": "#/$defs/inherit"} - environment-pass: {"$ref": "#/$defs/inherit"} - repair-wheel-command: {"$ref": "#/$defs/inherit"} - test-command: {"$ref": "#/$defs/inherit"} - test-extras: {"$ref": "#/$defs/inherit"} - test-sources: {"$ref": "#/$defs/inherit"} - test-requires: {"$ref": "#/$defs/inherit"} - test-environment: {"$ref": "#/$defs/inherit"} - test-runtime: {"$ref": "#/$defs/inherit"} + description: Merge values with lower-precedence layers instead of replacing them. + properties: {} audit-command: description: Execute a shell command to audit each wheel after it is repaired. Use {wheel} for each wheel path, or {abi3_wheel} to only audit abi3 wheels. type: string_array @@ -343,7 +326,24 @@ for key, value in schema["properties"].items(): value["title"] = f"CIBW_{key.replace('-', '_').upper()}" + +def inherit_ref_table(option_names: dict[str, Any]) -> dict[str, Any]: + return { + name: {"$ref": "#/$defs/inherit"} + for name in option_names + if name not in {"inherit", "select"} + } + + +# any option can carry an inherit rule; derive the keys from the option list +# so the schema stays in sync with the runtime (must run before the overrides +# and platform sections are merged into the properties below) +schema["properties"]["inherit"]["properties"] = inherit_ref_table(schema["properties"]) + non_global_options = {k: {"$ref": f"#/properties/{k}"} for k in schema["properties"]} +# placeholder pinning the key's position; each section's real (narrower) +# inherit table is filled in below, once the section's option set is final +non_global_options["inherit"] = {} del non_global_options["build"] del non_global_options["skip"] del non_global_options["test-skip"] @@ -393,6 +393,14 @@ def as_object(d: dict[str, Any]) -> dict[str, Any]: del oses["linux"]["properties"]["dependency-versions"] +# each section only offers inherit rules for the options it can set +for section in (overrides["items"], *oses.values()): + section["properties"]["inherit"] = { + "type": "object", + "additionalProperties": False, + "properties": inherit_ref_table(section["properties"]), + } + schema["properties"]["overrides"] = overrides schema["properties"] |= oses diff --git a/cibuildwheel/options.py b/cibuildwheel/options.py index afac21d06..c05f07363 100644 --- a/cibuildwheel/options.py +++ b/cibuildwheel/options.py @@ -396,17 +396,21 @@ def _apply_inherit_rule( # if after is an empty string, we shouldn't add any separator return before + msg = f"Don't know how to merge {before!r} and {after!r} with {rule}" + if not option_format: - msg = f"Don't know how to merge {before!r} and {after!r} with {rule}" raise OptionsReaderError(msg) - match rule: - case InheritRule.APPEND: - return option_format.merge_values(before, after) - case InheritRule.PREPEND: - return option_format.merge_values(after, before) - case _: - assert_never(rule) + try: + match rule: + case InheritRule.APPEND: + return option_format.merge_values(before, after) + case InheritRule.PREPEND: + return option_format.merge_values(after, before) + case _: + assert_never(rule) + except OptionFormat.NotSupported: + raise OptionsReaderError(msg) from None def _stringify_setting( @@ -442,26 +446,50 @@ def _stringify_setting( return setting -def parse_inherit(config: str | dict[str, str] | None) -> dict[str, InheritRule]: - inherit_dict: dict[str, str] +def _suggestion(name: str, allowed_names: Set[str]) -> str: + """A hint like " Perhaps you meant 'x'?" naming the closest match, or "".""" + matches = difflib.get_close_matches(name, allowed_names, 1, 0.7) + return f" Perhaps you meant {matches[0]!r}?" if matches else "" + + +def parse_inherit( + config: str | dict[str, str] | None, option_names: Set[str] +) -> dict[str, InheritRule]: + inherit_dict: dict[str, Any] if config is None: return {} if isinstance(config, str): - parsed = parse_arbitrary_key_value_string(config, default_value="append") - inherit_dict = {k: "".join(v) for k, v in parsed.items()} + try: + parsed = parse_arbitrary_key_value_string(config, default_value="append") + except ValueError as e: + raise OptionsReaderError(str(e)) from e + + for key, values in parsed.items(): + if len(values) != 1: + msg = f"'inherit' rule for {key!r} must be a single value, got {values!r}" + raise OptionsReaderError(msg) + + inherit_dict = {k: v[0] for k, v in parsed.items()} elif isinstance(config, dict): inherit_dict = config else: msg = "'inherit' must be a string or a table" raise OptionsReaderError(msg) - if not all(v in {"none", "append", "prepend"} for v in inherit_dict.values()): - msg = "'inherit' must contain only {'none', 'append', 'prepend'} values" - raise OptionsReaderError(msg) + rules = {} + for name, value in inherit_dict.items(): + if name not in option_names: + msg = f"Option {name!r} not supported in 'inherit'.{_suggestion(name, option_names)}" + raise OptionsReaderError(msg) + if not isinstance(value, str) or value.upper() not in InheritRule.__members__: + valid_rules = ", ".join(repr(rule.name.lower()) for rule in InheritRule) + msg = f"'inherit' rule for {name!r} must be one of {valid_rules}, got {value!r}" + raise OptionsReaderError(msg) + rules[name] = InheritRule[value.upper()] - return {k: InheritRule[v.upper()] for k, v in inherit_dict.items()} + return rules class OptionsReader: @@ -512,9 +540,25 @@ def __init__( self._validate_platform_option(option_name) self.config_options = config_options - self.config_options_inherit = parse_inherit(config_options.get("inherit")) self.config_platform_options = config_platform_options - self.config_platform_options_inherit = parse_inherit(config_platform_options.get("inherit")) + + # the option names that 'inherit' rules may refer to + self._option_names = ( + self.default_options.keys() | self.default_platform_options.keys() + ) - PLATFORMS + + self.config_options_inherit = parse_inherit( + config_options.get("inherit"), self._option_names + ) + self.config_platform_options_inherit = parse_inherit( + config_platform_options.get("inherit"), self._option_names + ) + + # all validation happens eagerly, so that malformed config fails fast + # even on code paths that never resolve per-identifier options + self.env_inherit = self._parse_env_inherit("CIBW_INHERIT") + self.env_platform_inherit = self._parse_env_inherit(f"CIBW_INHERIT_{platform.upper()}") + self.overrides = self._parse_overrides() self.current_identifier: str | None = None @@ -526,10 +570,10 @@ def _validate_global_option(self, name: str) -> None: allowed_option_names = self.default_options.keys() | PLATFORMS | {"inherit", "overrides"} if name not in allowed_option_names: - msg = f"Option {name!r} not supported in a config file." - matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7) - if matches: - msg += f" Perhaps you meant {matches[0]!r}?" + msg = ( + f"Option {name!r} not supported in a config file." + f"{_suggestion(name, allowed_option_names)}" + ) raise OptionsReaderError(msg) def _validate_platform_option(self, name: str) -> None: @@ -547,10 +591,10 @@ def _validate_platform_option(self, name: str) -> None: ) if name not in allowed_option_names: - msg = f"Option {name!r} not supported in the {self.platform!r} section" - matches = difflib.get_close_matches(name, allowed_option_names, 1, 0.7) - if matches: - msg += f" Perhaps you meant {matches[0]!r}?" + msg = ( + f"Option {name!r} not supported in the {self.platform!r} section" + f"{_suggestion(name, allowed_option_names)}" + ) raise OptionsReaderError(msg) def _load_file(self, filename: Path) -> tuple[dict[str, Any], dict[str, Any]]: @@ -565,8 +609,7 @@ def _load_file(self, filename: Path) -> tuple[dict[str, Any], dict[str, Any]]: return global_options, platform_options - @functools.cached_property - def overrides(self) -> list[Override]: + def _parse_overrides(self) -> list[Override]: config_overrides = self.config_options.get("overrides") overrides: list[Override] = [] @@ -587,34 +630,19 @@ def overrides(self) -> list[Override]: inherit = config_override.pop("inherit", {}) - overrides.append(Override(select, config_override, parse_inherit(inherit))) + overrides.append( + Override(select, config_override, parse_inherit(inherit, self._option_names)) + ) return overrides - @functools.cached_property - def env_inherit(self) -> dict[str, InheritRule]: - env_inherit_str = self.env.get("CIBW_INHERIT", "") + def _parse_env_inherit(self, envvar: str) -> dict[str, InheritRule]: try: - return parse_inherit(env_inherit_str) + return parse_inherit(self.env.get(envvar), self._option_names) except OptionsReaderError as e: - msg = f"Failed to parse CIBW_INHERIT environment variable. {e}" + msg = f"Failed to parse {envvar} environment variable. {e}" raise errors.ConfigurationError(msg) from e - @functools.cached_property - def env_platform_inherit(self) -> dict[str, InheritRule]: - env_inherit = self.env_inherit - - # find the rules which have -{platform} on the end of their key, - # remove the platform suffix from the key and return the resulting - # rule. - platform_suffix = f"-{self.platform}" - - return { - key.removesuffix(platform_suffix): value - for key, value in env_inherit.items() - if key.endswith(platform_suffix) - } - @property def active_config_overrides(self) -> list[Override]: if self.current_identifier is None: @@ -661,15 +689,14 @@ def get( # get the option from the default, then the config file, then finally the environment. # platform-specific options are preferred, if they're allowed. + env_rule = self.env_inherit.get(name, default_env_rule) + # CIBW_INHERIT_ rules win; CIBW_INHERIT rules also + # apply to the platform variable when no platform rule is set + plat_env_rule = self.env_platform_inherit.get(name, env_rule) + return _resolve_cascade( - ( - self.default_options.get(name), - InheritRule.NONE, - ), - ( - self.default_platform_options.get(name), - InheritRule.NONE, - ), + (self.default_options.get(name), InheritRule.NONE), + (self.default_platform_options.get(name), InheritRule.NONE), ( self.config_options.get(name), self.config_options_inherit.get(name, InheritRule.NONE), @@ -679,20 +706,11 @@ def get( self.config_platform_options_inherit.get(name, InheritRule.NONE), ), *[ - ( - o.options.get(name), - o.inherit.get(name, InheritRule.NONE), - ) + (o.options.get(name), o.inherit.get(name, InheritRule.NONE)) for o in self.active_config_overrides ], - ( - self.env.get(envvar), - self.env_inherit.get(name, default_env_rule), - ), - ( - self.env.get(plat_envvar) if env_plat else None, - self.env_platform_inherit.get(name, default_env_rule), - ), + (self.env.get(envvar), env_rule), + (self.env.get(plat_envvar) if env_plat else None, plat_env_rule), ignore_empty=ignore_empty, option_format=option_format, ) @@ -862,7 +880,7 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions: test_command = self.reader.get("test-command", option_format=ListFormat(sep=" && ")) before_test = self.reader.get("before-test", option_format=ListFormat(sep=" && ")) - xbuild_tools: list[str] | None = shlex.split( + raw_xbuild_tools = shlex.split( self.reader.get( "xbuild-tools", option_format=ListFormat(sep=" ", quote=shlex.quote) ) @@ -871,8 +889,12 @@ def _compute_build_options(self, identifier: str | None) -> BuildOptions: # doesn't have an explicit NULL value. If xbuild-tools is set to the # sentinel, it indicates that the user hasn't defined xbuild-tools # *at all* (not even an `xbuild-tools = []` definition). - if xbuild_tools == ["\u0000"]: + xbuild_tools: list[str] | None + if raw_xbuild_tools == ["\u0000"]: xbuild_tools = None + else: + # an inherit rule may have merged real values with the sentinel + xbuild_tools = [tool for tool in raw_xbuild_tools if tool != "\u0000"] xbuild_files = parse_arbitrary_key_value_string( self.reader.get( diff --git a/cibuildwheel/resources/cibuildwheel.schema.json b/cibuildwheel/resources/cibuildwheel.schema.json index 7bce36d36..23323d559 100644 --- a/cibuildwheel/resources/cibuildwheel.schema.json +++ b/cibuildwheel/resources/cibuildwheel.schema.json @@ -29,6 +29,7 @@ "inherit": { "type": "object", "additionalProperties": false, + "description": "Merge values with lower-precedence layers instead of replacing them.", "properties": { "audit-command": { "$ref": "#/$defs/inherit" @@ -36,19 +37,25 @@ "audit-requires": { "$ref": "#/$defs/inherit" }, + "archs": { + "$ref": "#/$defs/inherit" + }, "before-all": { "$ref": "#/$defs/inherit" }, "before-build": { "$ref": "#/$defs/inherit" }, - "xbuild-tools": { + "before-test": { "$ref": "#/$defs/inherit" }, - "xbuild-files": { + "build": { "$ref": "#/$defs/inherit" }, - "before-test": { + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { "$ref": "#/$defs/inherit" }, "config-settings": { @@ -57,15 +64,84 @@ "container-engine": { "$ref": "#/$defs/inherit" }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "enable": { + "$ref": "#/$defs/inherit" + }, "environment": { "$ref": "#/$defs/inherit" }, "environment-pass": { "$ref": "#/$defs/inherit" }, + "manylinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, "repair-wheel-command": { "$ref": "#/$defs/inherit" }, + "skip": { + "$ref": "#/$defs/inherit" + }, "test-command": { "$ref": "#/$defs/inherit" }, @@ -75,9 +151,15 @@ "test-sources": { "$ref": "#/$defs/inherit" }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, "test-requires": { "$ref": "#/$defs/inherit" }, + "test-skip": { + "$ref": "#/$defs/inherit" + }, "test-environment": { "$ref": "#/$defs/inherit" }, @@ -750,7 +832,130 @@ ] }, "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "container-engine": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "environment-pass": { + "$ref": "#/$defs/inherit" + }, + "manylinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -880,7 +1085,130 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "container-engine": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "environment-pass": { + "$ref": "#/$defs/inherit" + }, + "manylinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_i686-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-pypy_x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "manylinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-aarch64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-armv7l-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-i686-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-ppc64le-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-riscv64-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-s390x-image": { + "$ref": "#/$defs/inherit" + }, + "musllinux-x86_64-image": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -1022,7 +1350,76 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -1110,7 +1507,76 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -1198,7 +1664,76 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -1273,7 +1808,76 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" @@ -1361,7 +1965,76 @@ "additionalProperties": false, "properties": { "inherit": { - "$ref": "#/properties/inherit" + "type": "object", + "additionalProperties": false, + "properties": { + "audit-command": { + "$ref": "#/$defs/inherit" + }, + "audit-requires": { + "$ref": "#/$defs/inherit" + }, + "archs": { + "$ref": "#/$defs/inherit" + }, + "before-all": { + "$ref": "#/$defs/inherit" + }, + "before-build": { + "$ref": "#/$defs/inherit" + }, + "before-test": { + "$ref": "#/$defs/inherit" + }, + "build-frontend": { + "$ref": "#/$defs/inherit" + }, + "build-verbosity": { + "$ref": "#/$defs/inherit" + }, + "config-settings": { + "$ref": "#/$defs/inherit" + }, + "dependency-versions": { + "$ref": "#/$defs/inherit" + }, + "environment": { + "$ref": "#/$defs/inherit" + }, + "xbuild-tools": { + "$ref": "#/$defs/inherit" + }, + "xbuild-files": { + "$ref": "#/$defs/inherit" + }, + "pyodide-version": { + "$ref": "#/$defs/inherit" + }, + "repair-wheel-command": { + "$ref": "#/$defs/inherit" + }, + "test-command": { + "$ref": "#/$defs/inherit" + }, + "test-extras": { + "$ref": "#/$defs/inherit" + }, + "test-sources": { + "$ref": "#/$defs/inherit" + }, + "test-groups": { + "$ref": "#/$defs/inherit" + }, + "test-requires": { + "$ref": "#/$defs/inherit" + }, + "test-environment": { + "$ref": "#/$defs/inherit" + }, + "test-runtime": { + "$ref": "#/$defs/inherit" + } + } }, "audit-command": { "$ref": "#/properties/audit-command" diff --git a/cibuildwheel/util/helpers.py b/cibuildwheel/util/helpers.py index 1121c7244..41f00fa5b 100644 --- a/cibuildwheel/util/helpers.py +++ b/cibuildwheel/util/helpers.py @@ -102,6 +102,9 @@ def parse_key_value_string( key_value_string: str, positional_arg_names: Sequence[str] | None = None, kw_arg_names: Sequence[str] | None = None, + *, + arbitrary_keys: bool = False, + default_value: str | None = None, ) -> dict[str, list[str]]: """ Parses a string like "docker; create_args: --some-option=value another-option" @@ -128,11 +131,19 @@ def parse_key_value_string( # check to see if the option name is specified field_name, sep, first_value = field[0].partition(":") if sep: - if field_name not in all_field_names: + if not arbitrary_keys and 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) - values = ([first_value] if first_value else []) + field[1:] + result[field_name] += ([first_value] if first_value else []) + field[1:] + elif arbitrary_keys: + # bare words are keys without values + if default_value is None: + msg = f"Failed to parse {key_value_string!r}. No value specified for {field_name!r}. Expected ':' followed by a value." + raise ValueError(msg) + + for key in field: + result[key].append(default_value) else: try: field_name = positional_arg_names[field_i] @@ -140,9 +151,7 @@ def parse_key_value_string( msg = f"Failed to parse {key_value_string!r}. Too many positional arguments - expected a maximum of {len(positional_arg_names)}" raise ValueError(msg) from None - values = field - - result[field_name] += values + result[field_name] += field return dict(result) @@ -163,33 +172,9 @@ def parse_arbitrary_key_value_string( interpreted as keys. Keys without a value will be assigned the default_value if provided, otherwise throw an error. """ - shlexer = shlex.shlex(key_value_string, posix=True, punctuation_chars=";") - shlexer.commenters = "" - shlexer.whitespace_split = True - parts = list(shlexer) - # parts now looks like - # ['before-build', ';', 'before-test:', 'append', ';', 'after-test:', 'prepend'] - - # split by semicolon - result: defaultdict[str, list[str]] = defaultdict(list) - fields = [list(group) for k, group in itertools.groupby(parts, lambda x: x == ";") if not k] - for field in fields: - # check to see if the option name is specified - field_name, sep, first_value = field[0].partition(":") - if sep: - # the colon was present, so the first value is the value after the colon - values = ([first_value] if first_value else []) + field[1:] - result[field_name] += values - else: - # no colon, so it's a key (or set of keys) without values - if default_value is None: - msg = f"Failed to parse {key_value_string!r}. No value specified for {field_name!r}. Expected ':' followed by a value." - raise ValueError(msg) - - for key in field: - result[key].append(default_value) - - return dict(result) + return parse_key_value_string( + key_value_string, arbitrary_keys=True, default_value=default_value + ) @dataclasses.dataclass(order=True) diff --git a/docs/configuration.md b/docs/configuration.md index 6a3576383..cb1f9d397 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -259,11 +259,14 @@ CIBW_AUDIT_COMMAND: "twine check {wheel}" CIBW_INHERIT: "audit-requires; audit-command" ``` -To control a platform-specific environment variable, add the lowercase platform -suffix to the option name. For example, this prepends `CIBW_BEFORE_ALL_LINUX` to -the value accumulated from the lower-precedence layers: +Rules in `CIBW_INHERIT` apply to both the plain `CIBW_