Skip to content

Commit 17ac308

Browse files
committed
Add variant support, wheel build functional
1 parent c5bbb16 commit 17ac308

File tree

5 files changed

+136
-108
lines changed

5 files changed

+136
-108
lines changed

backend/src/hatchling/build.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,9 @@ def prepare_metadata_for_build_wheel(
118118
with open(os.path.join(directory, 'METADATA'), 'w', encoding='utf-8') as f:
119119
f.write(builder.config.core_metadata_constructor(builder.metadata))
120120

121-
with open(os.path.join(directory, VARIANT_DIST_INFO_FILENAME), 'w', encoding='utf-8') as f:
122-
f.write(builder.config.variants_json_constructor(builder.metadata))
121+
if builder.metadata.variant_hash is not None:
122+
with open(os.path.join(directory, VARIANT_DIST_INFO_FILENAME), 'w', encoding='utf-8') as f:
123+
f.write(builder.config.variants_json_constructor(builder.metadata.variant_config))
123124

124125
return os.path.basename(directory)
125126

backend/src/hatchling/builders/wheel.py

Lines changed: 107 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@
1414
from typing import TYPE_CHECKING, Any, Callable, Iterable, NamedTuple, Sequence, Tuple, cast
1515

1616
if TYPE_CHECKING:
17+
from hatchling.bridge.app import Application
1718
from hatchling.metadata.core import ProjectMetadata
19+
from hatchling.plugin.manager import PluginManagerBound
1820

1921
from hatchling.__about__ import __version__
2022
from hatchling.builders.config import BuilderConfig
@@ -47,6 +49,7 @@
4749
VARIANTS_JSON_SCHEMA_URL,
4850
VARIANTS_JSON_VARIANT_DATA_KEY,
4951
)
52+
from hatchling.metadata.core import VariantConfig
5053
from hatchling.metadata.spec import DEFAULT_METADATA_VERSION, get_core_metadata_constructors
5154

5255
if TYPE_CHECKING:
@@ -307,75 +310,73 @@ def core_metadata_constructor(self) -> Callable[..., str]:
307310
@property
308311
def variants_json_constructor(self) -> Callable[..., str]:
309312
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
379380
return self.__variants_json_constructor
380381

381382
@property
@@ -545,6 +546,32 @@ class WheelBuilder(BuilderInterface):
545546

546547
PLUGIN_NAME = 'wheel'
547548

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+
548575
def get_version_api(self) -> dict[str, Callable]:
549576
return {'standard': self.build_standard, 'editable': self.build_editable}
550577

@@ -579,7 +606,11 @@ def build_standard(self, directory: str, **build_data: Any) -> str:
579606
records.write((f'{archive.metadata_directory}/RECORD', '', ''))
580607
archive.write_metadata('RECORD', records.construct())
581608

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)
583614

584615
replace_file(archive.path, target)
585616
normalize_artifact_permissions(target)
@@ -806,11 +837,12 @@ def write_project_metadata(
806837
'METADATA', self.config.core_metadata_constructor(self.metadata, extra_dependencies=extra_dependencies)
807838
)
808839
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)
814846

815847
def add_licenses(self, archive: WheelArchive, records: RecordFile) -> None:
816848
for relative_path in self.metadata.core.license_files:

backend/src/hatchling/cli/build/__init__.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,10 @@ def build_impl(
1515
clean_hooks_after: bool,
1616
clean_only: bool,
1717
show_dynamic_deps: bool,
18-
variant_props: list[str],
19-
variant_null: bool,
18+
variant_props: list[str] | None = None,
19+
variant_null: bool = False,
2020
variant_label: str | None,
2121
) -> None:
22-
print(f"{__file__}::build_impl")
23-
print(f'{variant_props=}')
24-
print(f'{variant_null=}')
25-
print(f'{variant_label=}')
26-
2722
import os
2823

2924
from hatchling.bridge.app import Application
@@ -80,8 +75,20 @@ def build_impl(
8075
if not (clean_only or show_dynamic_deps) and len(target_data) > 1:
8176
app.display_mini_header(target_name)
8277

83-
print(f"{metadata=}")
84-
builder = builder_class(root, plugin_manager=plugin_manager, metadata=metadata, app=app.get_safe_application())
78+
variant_build_kwargs = {}
79+
if f'{builder_class.__module__}.{builder_class.__name__}' == 'hatchling.builders.wheel.WheelBuilder':
80+
variant_build_kwargs = {
81+
"variant_props": variant_props if not variant_null else [],
82+
"variant_label": variant_label,
83+
}
84+
85+
builder = builder_class(
86+
root,
87+
plugin_manager=plugin_manager,
88+
metadata=metadata,
89+
app=app.get_safe_application(),
90+
**variant_build_kwargs,
91+
)
8592

8693
if show_dynamic_deps:
8794
for dependency in builder.config.dynamic_dependencies:

backend/src/hatchling/metadata/core.py

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -127,27 +127,6 @@ def validate(self):
127127
for provider_cfg in self.providers.values():
128128
provider_cfg.validate()
129129

130-
def to_variant_cfg_dict(self) -> dict[str, Any]:
131-
"""Converts the VariantConfig instance to a metadata dictionary."""
132-
return {
133-
"variant_hash": self.vhash,
134-
"variant_properties": self.properties,
135-
"variant_plugins": {
136-
namespace: {
137-
"requires": provider_cfg.requires,
138-
"plugin_api": provider_cfg.plugin_api,
139-
"enable_if": provider_cfg.enable_if,
140-
"optional": provider_cfg.optional,
141-
}
142-
for namespace, provider_cfg in self.providers.items()
143-
},
144-
"variant_default_priorities": {
145-
"namespace": self.default_priorities.get("namespace", []),
146-
"feature": self.default_priorities.get("feature", {}),
147-
"property": self.default_priorities.get("property", {})
148-
}
149-
}
150-
151130

152131
class ProjectMetadata(Generic[PluginManagerBound]):
153132
def __init__(
@@ -172,6 +151,8 @@ def __init__(
172151
self._project_file: str | None = None
173152

174153
self.variant_hash: str | None = None
154+
self.variant_config: VariantConfig | None = None
155+
self._variant_config_data: dict[str, Any] | None = None
175156

176157
# App already loaded config
177158
if config is not None and root is not None:
@@ -241,6 +222,13 @@ def dynamic(self) -> list[str]:
241222

242223
return self._dynamic
243224

225+
@property
226+
def variant_config_data(self) -> dict[str, Any]:
227+
"""Variant configuration data fetched from pyproject.toml"""
228+
if self._variant_config_data is None:
229+
self._variant_config_data = self.config.get('variant', {})
230+
return self._variant_config_data
231+
244232
@property
245233
def name(self) -> str:
246234
# Duplicate the name parsing here for situations where it's

src/hatch/cli/build/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def build(
127127
else str(artifact_path)
128128
)
129129
else:
130-
command = ['/workspace/.venv/bin/python', '-u', '-m', 'hatchling', 'build', '--target', target]
130+
command = ['python', '-u', '-m', 'hatchling', 'build', '--target', target]
131131
# Pass variant flags to Hatchling
132132
for prop in variant_props:
133133
command.extend(['-p', prop])

0 commit comments

Comments
 (0)