| 
14 | 14 | from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Sequence, Tuple, cast  | 
15 | 15 | 
 
  | 
16 | 16 | if TYPE_CHECKING:  | 
 | 17 | +    from hatchling.bridge.app import Application  | 
17 | 18 |     from hatchling.metadata.core import ProjectMetadata  | 
 | 19 | +    from hatchling.plugin.manager import PluginManagerBound  | 
18 | 20 | 
 
  | 
19 | 21 | from hatchling.__about__ import __version__  | 
20 | 22 | from hatchling.builders.config import BuilderConfig  | 
 | 
47 | 49 |     VARIANTS_JSON_SCHEMA_URL,  | 
48 | 50 |     VARIANTS_JSON_VARIANT_DATA_KEY,  | 
49 | 51 | )  | 
 | 52 | +from hatchling.metadata.core import VariantConfig  | 
50 | 53 | from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors  | 
51 | 54 | 
 
  | 
52 | 55 | if TYPE_CHECKING:  | 
@@ -307,75 +310,73 @@ def core_metadata_constructor(self) -> Callable[..., str]:  | 
307 | 310 |     @property  | 
308 | 311 |     def variants_json_constructor(self) -> Callable[..., str]:  | 
309 | 312 |         if self.__variants_json_constructor is None:  | 
310 |  | -            def constructor(metadata: ProjectMetadata) -> str:  | 
311 |  | -                if metadata.variant_hash is not None:  | 
312 |  | -                    data = {  | 
313 |  | -                        VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL,  | 
314 |  | -                        VARIANT_INFO_DEFAULT_PRIO_KEY: {},  | 
315 |  | -                        VARIANT_INFO_PROVIDER_DATA_KEY: {},  | 
316 |  | -                        VARIANTS_JSON_VARIANT_DATA_KEY: {}  | 
317 |  | -                    }  | 
318 |  | - | 
319 |  | -                    # ==================== VARIANT_INFO_DEFAULT_PRIO_KEY ==================== #  | 
320 |  | - | 
321 |  | -                    if (ns_prio := metadata.variant_default_priorities["namespace"]):  | 
322 |  | -                        data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ns_prio  | 
323 |  | - | 
324 |  | -                    if (feat_prio := metadata.variant_default_priorities["feature"]):  | 
325 |  | -                        data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_FEATURE_KEY] = feat_prio  | 
326 |  | - | 
327 |  | -                    if (prop_prio := metadata.variant_default_priorities["property"]):  | 
328 |  | -                        data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_PROPERTY_KEY] = prop_prio  | 
329 |  | - | 
330 |  | -                    if not data[VARIANT_INFO_DEFAULT_PRIO_KEY]:  | 
331 |  | -                        # If no default priorities are set, remove the key  | 
332 |  | -                        del data[VARIANT_INFO_DEFAULT_PRIO_KEY]  | 
333 |  | - | 
334 |  | -                    # ==================== VARIANT_INFO_PROVIDER_DATA_KEY ==================== #  | 
335 |  | - | 
336 |  | -                    variant_providers = defaultdict(dict)  | 
337 |  | -                    for ns, plugin_conf in metadata.variant_plugins.items():  | 
338 |  | -                        variant_providers[ns][VARIANT_INFO_PROVIDER_REQUIRES_KEY] = plugin_conf.get("requires", [])  | 
339 |  | - | 
340 |  | -                        if (enable_if := plugin_conf.get("enable_if", None)) is not None:  | 
341 |  | -                            variant_providers[ns][VARIANT_INFO_PROVIDER_ENABLE_IF_KEY] = enable_if  | 
342 |  | - | 
343 |  | -                        if plugin_conf.get("optional", False):  | 
344 |  | -                            variant_providers[ns][VARIANT_INFO_PROVIDER_OPTIONAL_KEY] = True  | 
345 |  | - | 
346 |  | -                        if (plugin_api := plugin_conf.get("plugin_api", None)) is not None:  | 
347 |  | -                            variant_providers[ns][VARIANT_INFO_PROVIDER_PLUGIN_API_KEY] = plugin_api  | 
348 |  | - | 
349 |  | -                    data[VARIANT_INFO_PROVIDER_DATA_KEY] = variant_providers  | 
350 |  | - | 
351 |  | -                    # ==================== VARIANTS_JSON_VARIANT_DATA_KEY ==================== #  | 
352 |  | - | 
353 |  | -                    variant_data = defaultdict(lambda: defaultdict(set))  | 
354 |  | -                    for vprop_str in metadata.variant_properties:  | 
355 |  | -                        match = VALIDATION_PROPERTY_REGEX.match(vprop_str)  | 
356 |  | -                        if not match:  | 
357 |  | -                            raise ValueError(  | 
358 |  | -                                f"Invalid variant property '{vprop_str}' in variant {metadata.variant_hash}"  | 
359 |  | -                            )  | 
360 |  | -                        namespace = match.group('namespace')  | 
361 |  | -                        feature = match.group('feature')  | 
362 |  | -                        value = match.group('value')  | 
363 |  | -                        variant_data[namespace][feature].add(value)  | 
364 |  | -                    data[VARIANTS_JSON_VARIANT_DATA_KEY][metadata.variant_hash] = variant_data  | 
365 |  | - | 
366 |  | -                    def preprocess(data):  | 
367 |  | -                        """Preprocess the data to ensure it is JSON serializable."""  | 
368 |  | -                        if isinstance(data, (defaultdict, dict)):  | 
369 |  | -                            return {k: preprocess(v) for k, v in data.items()}  | 
370 |  | -                        if isinstance(data, set):  | 
371 |  | -                            return list(data)  | 
372 |  | -                        return data  | 
373 |  | - | 
374 |  | -                    return json.dumps(  | 
375 |  | -                        preprocess(data), indent=4, sort_keys=True, ensure_ascii=False  | 
376 |  | -                    )  | 
377 |  | -                return ''  | 
378 |  | -            self.__variants_json_constructor = constructor  | 
 | 313 | +            def constructor(variant_config: VariantConfig) -> str:  | 
 | 314 | +                data = {  | 
 | 315 | +                    VARIANTS_JSON_SCHEMA_KEY: VARIANTS_JSON_SCHEMA_URL,  | 
 | 316 | +                    VARIANT_INFO_DEFAULT_PRIO_KEY: {},  | 
 | 317 | +                    VARIANT_INFO_PROVIDER_DATA_KEY: {},  | 
 | 318 | +                    VARIANTS_JSON_VARIANT_DATA_KEY: {}  | 
 | 319 | +                }  | 
 | 320 | + | 
 | 321 | +                # ==================== VARIANT_INFO_DEFAULT_PRIO_KEY ==================== #  | 
 | 322 | + | 
 | 323 | +                if (ns_prio := variant_config.default_priorities["namespace"]):  | 
 | 324 | +                    data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_NAMESPACE_KEY] = ns_prio  | 
 | 325 | + | 
 | 326 | +                if (feat_prio := variant_config.default_priorities["feature"]):  | 
 | 327 | +                    data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_FEATURE_KEY] = feat_prio  | 
 | 328 | + | 
 | 329 | +                if (prop_prio := variant_config.default_priorities["property"]):  | 
 | 330 | +                    data[VARIANT_INFO_DEFAULT_PRIO_KEY][VARIANT_INFO_PROPERTY_KEY] = prop_prio  | 
 | 331 | + | 
 | 332 | +                if not data[VARIANT_INFO_DEFAULT_PRIO_KEY]:  | 
 | 333 | +                    # If no default priorities are set, remove the key  | 
 | 334 | +                    del data[VARIANT_INFO_DEFAULT_PRIO_KEY]  | 
 | 335 | + | 
 | 336 | +                # ==================== VARIANT_INFO_PROVIDER_DATA_KEY ==================== #  | 
 | 337 | + | 
 | 338 | +                variant_providers = defaultdict(dict)  | 
 | 339 | +                for ns, provider_cfg in variant_config.providers.items():  | 
 | 340 | +                    variant_providers[ns][VARIANT_INFO_PROVIDER_REQUIRES_KEY] = provider_cfg.requires  | 
 | 341 | + | 
 | 342 | +                    if provider_cfg.enable_if is not None:  | 
 | 343 | +                        variant_providers[ns][VARIANT_INFO_PROVIDER_ENABLE_IF_KEY] = provider_cfg.enable_if  | 
 | 344 | + | 
 | 345 | +                    if provider_cfg.optional:  | 
 | 346 | +                        variant_providers[ns][VARIANT_INFO_PROVIDER_OPTIONAL_KEY] = True  | 
 | 347 | + | 
 | 348 | +                    if provider_cfg.plugin_api is not None:  | 
 | 349 | +                        variant_providers[ns][VARIANT_INFO_PROVIDER_PLUGIN_API_KEY] = provider_cfg.plugin_api  | 
 | 350 | + | 
 | 351 | +                data[VARIANT_INFO_PROVIDER_DATA_KEY] = variant_providers  | 
 | 352 | + | 
 | 353 | +                # ==================== VARIANTS_JSON_VARIANT_DATA_KEY ==================== #  | 
 | 354 | + | 
 | 355 | +                variant_data = defaultdict(lambda: defaultdict(set))  | 
 | 356 | +                for vprop_str in (variant_config.properties or []):  | 
 | 357 | +                    match = VALIDATION_PROPERTY_REGEX.match(vprop_str)  | 
 | 358 | +                    if not match:  | 
 | 359 | +                        raise ValueError(  | 
 | 360 | +                            f"Invalid variant property '{vprop_str}' in variant {variant_config.variant_hash}"  | 
 | 361 | +                        )  | 
 | 362 | +                    namespace = match.group('namespace')  | 
 | 363 | +                    feature = match.group('feature')  | 
 | 364 | +                    value = match.group('value')  | 
 | 365 | +                    variant_data[namespace][feature].add(value)  | 
 | 366 | +                data[VARIANTS_JSON_VARIANT_DATA_KEY][variant_config.vhash] = variant_data  | 
 | 367 | + | 
 | 368 | +                def preprocess(data):  | 
 | 369 | +                    """Preprocess the data to ensure it is JSON serializable."""  | 
 | 370 | +                    if isinstance(data, (defaultdict, dict)):  | 
 | 371 | +                        return {k: preprocess(v) for k, v in data.items()}  | 
 | 372 | +                    if isinstance(data, set):  | 
 | 373 | +                        return list(data)  | 
 | 374 | +                    return data  | 
 | 375 | + | 
 | 376 | +                return json.dumps(  | 
 | 377 | +                    preprocess(data), indent=4, sort_keys=True, ensure_ascii=False  | 
 | 378 | +                )  | 
 | 379 | +        self.__variants_json_constructor = constructor  | 
379 | 380 |         return self.__variants_json_constructor  | 
380 | 381 | 
 
  | 
381 | 382 |     @property  | 
@@ -545,6 +546,32 @@ class WheelBuilder(BuilderInterface):  | 
545 | 546 | 
 
  | 
546 | 547 |     PLUGIN_NAME = 'wheel'  | 
547 | 548 | 
 
  | 
 | 549 | +    def __init__(  | 
 | 550 | +        self,  | 
 | 551 | +        root: str,  | 
 | 552 | +        plugin_manager: PluginManagerBound | None = None,  | 
 | 553 | +        config: dict[str, Any] | None = None,  | 
 | 554 | +        metadata: ProjectMetadata | None = None,  | 
 | 555 | +        app: Application | None = None,  | 
 | 556 | +        variant_props: list[str] | None = None,  | 
 | 557 | +        variant_label: str | None = None,  | 
 | 558 | +    ):  | 
 | 559 | +        metadata.variant_config = VariantConfig.from_dict(  | 
 | 560 | +            data=metadata.variant_config_data,  | 
 | 561 | +            vprops=variant_props,  | 
 | 562 | +            variant_label=variant_label,  | 
 | 563 | +        )  | 
 | 564 | +        metadata.variant_config.validate()  | 
 | 565 | +        metadata.variant_hash = metadata.variant_config.vhash  | 
 | 566 | + | 
 | 567 | +        super().__init__(  | 
 | 568 | +            root=root,  | 
 | 569 | +            plugin_manager=plugin_manager,  | 
 | 570 | +            config=config,  | 
 | 571 | +            metadata=metadata,  | 
 | 572 | +            app=app,  | 
 | 573 | +        )  | 
 | 574 | + | 
548 | 575 |     def get_version_api(self) -> dict[str, Callable]:  | 
549 | 576 |         return {'standard': self.build_standard, 'editable': self.build_editable}  | 
550 | 577 | 
 
  | 
@@ -579,7 +606,11 @@ def build_standard(self, directory: str, **build_data: Any) -> str:  | 
579 | 606 |             records.write((f'{archive.metadata_directory}/RECORD', '', ''))  | 
580 | 607 |             archive.write_metadata('RECORD', records.construct())  | 
581 | 608 | 
 
  | 
582 |  | -        target = os.path.join(directory, f"{self.artifact_project_id}-{build_data['tag']}.whl")  | 
 | 609 | +        if self.metadata.variant_hash is not None:  | 
 | 610 | +            wheel_name = f"{self.artifact_project_id}-{build_data['tag']}-{self.metadata.variant_hash}.whl"  | 
 | 611 | +        else:  | 
 | 612 | +            wheel_name = f"{self.artifact_project_id}-{build_data['tag']}.whl"  | 
 | 613 | +        target = os.path.join(directory, wheel_name)  | 
583 | 614 | 
 
  | 
584 | 615 |         replace_file(archive.path, target)  | 
585 | 616 |         normalize_artifact_permissions(target)  | 
@@ -806,11 +837,12 @@ def write_project_metadata(  | 
806 | 837 |             'METADATA', self.config.core_metadata_constructor(self.metadata, extra_dependencies=extra_dependencies)  | 
807 | 838 |         )  | 
808 | 839 |         records.write(record)  | 
809 |  | -        record = archive.write_metadata(  | 
810 |  | -            VARIANT_DIST_INFO_FILENAME,  | 
811 |  | -            self.config.variants_json_constructor(self.metadata),  | 
812 |  | -        )  | 
813 |  | -        records.write(record)  | 
 | 840 | +        if self.metadata.variant_hash is not None:  | 
 | 841 | +            record = archive.write_metadata(  | 
 | 842 | +                VARIANT_DIST_INFO_FILENAME,  | 
 | 843 | +                self.config.variants_json_constructor(self.metadata.variant_config),  | 
 | 844 | +            )  | 
 | 845 | +            records.write(record)  | 
814 | 846 | 
 
  | 
815 | 847 |     def add_licenses(self, archive: WheelArchive, records: RecordFile) -> None:  | 
816 | 848 |         for relative_path in self.metadata.core.license_files:  | 
 | 
0 commit comments