Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 23 additions & 5 deletions backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -339,11 +339,7 @@ def clean(self, directory: str, versions: list[str]) -> None:
def build_standard(self, directory: str, **build_data: Any) -> str:
if 'tag' not in build_data:
if build_data['infer_tag']:
from packaging.tags import sys_tags

best_matching_tag = next(sys_tags())
tag_parts = (best_matching_tag.interpreter, best_matching_tag.abi, best_matching_tag.platform)
build_data['tag'] = '-'.join(tag_parts)
build_data['tag'] = self.get_best_matching_tag()
else:
build_data['tag'] = self.get_default_tag()

Expand Down Expand Up @@ -588,6 +584,28 @@ def get_default_tag(self) -> str:

return f'{".".join(supported_python_versions)}-none-any'

def get_best_matching_tag(self) -> str:
import sys

from packaging.tags import sys_tags

tag = next(sys_tags())
tag_parts = [tag.interpreter, tag.abi, tag.platform]

archflags = os.environ.get('ARCHFLAGS', '')
if sys.platform == 'darwin' and archflags and sys.version_info[:2] >= (3, 8):
import platform
import re

archs = re.findall(r'-arch (\S+)', archflags)
if archs:
plat = tag_parts[2]
current_arch = platform.mac_ver()[2]
new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0]
tag_parts[2] = f'{plat[:plat.rfind(current_arch)]}{new_arch}'

return '-'.join(tag_parts)

def get_default_build_data(self) -> dict[str, Any]:
return {
'infer_tag': False,
Expand Down
1 change: 1 addition & 0 deletions docs/history/hatchling.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

***Fixed:***

- Acknowledge the `ARCHFLAGS` environment variable on macOS for the `wheel` target when build hooks set the `infer_tag` build data to `true`
- Fix dependency checking when encountering broken distributions
- Remove unnecessary encoding declaration in the default template for the `version` build hook

Expand Down
94 changes: 93 additions & 1 deletion tests/backend/builders/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

# https://github.com/python/cpython/pull/26184
fixed_pathlib_resolution = pytest.mark.skipif(
platform.system() == 'Windows' and (sys.version_info < (3, 8) or sys.implementation.name == 'pypy'),
sys.platform == 'win32' and (sys.version_info < (3, 8) or sys.implementation.name == 'pypy'),
reason='pathlib.Path.resolve has bug on Windows',
)

Expand Down Expand Up @@ -2796,3 +2796,95 @@ def test_editable_sources_rewrite_error(self, hatch, helpers, temp_dir):
),
):
list(builder.build(str(build_path)))

@pytest.mark.skipif(
sys.platform != 'darwin' or sys.version_info < (3, 8),
reason='requires support for ARM on macOS',
)
@pytest.mark.parametrize(
'archflags, expected_arch',
[('-arch x86_64', 'x86_64'), ('-arch arm64', 'arm64'), ('-arch arm64 -arch x86_64', 'universal2')],
)
def test_macos_archflags(self, hatch, helpers, temp_dir, config_file, archflags, expected_arch):
config_file.model.template.plugins['default']['src-layout'] = False
config_file.save()

project_name = 'My.App'

with temp_dir.as_cwd():
result = hatch('new', project_name)

assert result.exit_code == 0, result.output

project_path = temp_dir / 'my-app'

vcs_ignore_file = project_path / '.gitignore'
vcs_ignore_file.write_text('*.pyc\n*.so\n*.h')

build_script = project_path / DEFAULT_BUILD_SCRIPT
build_script.write_text(
helpers.dedent(
"""
import pathlib

from hatchling.builders.hooks.plugin.interface import BuildHookInterface

class CustomHook(BuildHookInterface):
def initialize(self, version, build_data):
build_data['pure_python'] = False
build_data['infer_tag'] = True

pathlib.Path('my_app', 'lib.so').touch()
pathlib.Path('my_app', 'lib.h').touch()
"""
)
)

config = {
'project': {'name': project_name, 'requires-python': '>3', 'dynamic': ['version']},
'tool': {
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
},
},
}
builder = WheelBuilder(str(project_path), config=config)

build_path = project_path / 'dist'
build_path.mkdir()

with project_path.as_cwd({'ARCHFLAGS': archflags}):
artifacts = list(builder.build(str(build_path)))

assert len(artifacts) == 1
expected_artifact = artifacts[0]

build_artifacts = list(build_path.iterdir())
assert len(build_artifacts) == 1
assert expected_artifact == str(build_artifacts[0])

tag = next(sys_tags())
tag_parts = [tag.interpreter, tag.abi, tag.platform]
tag_parts[2] = tag_parts[2].replace(platform.mac_ver()[2], expected_arch)
expected_tag = '-'.join(tag_parts)
assert expected_artifact == str(build_path / f'{builder.project_id}-{expected_tag}.whl')

extraction_directory = temp_dir / '_archive'
extraction_directory.mkdir()

with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
zip_archive.extractall(str(extraction_directory))

metadata_directory = f'{builder.project_id}.dist-info'
expected_files = helpers.get_template_files(
'wheel.standard_default_build_script_artifacts',
project_name,
metadata_directory=metadata_directory,
tag=expected_tag,
)
helpers.assert_files(extraction_directory, expected_files)