Skip to content

Add macos-max-compat option to the wheel target #699

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jan 3, 2023
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
40 changes: 32 additions & 8 deletions backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
self.__shared_data: dict[str, str] | None = None
self.__extra_metadata: dict[str, str] | None = None
self.__strict_naming: bool | None = None
self.__macos_max_compat: bool | None = None

def set_default_file_selection(self) -> None:
if self.__include or self.__exclude or self.__packages or self.__only_include:
Expand Down Expand Up @@ -325,6 +326,18 @@ def strict_naming(self) -> bool:

return self.__strict_naming

@property
def macos_max_compat(self) -> bool:
if self.__macos_max_compat is None:
macos_max_compat = self.target_config.get('macos-max-compat', True)
if not isinstance(macos_max_compat, bool):
message = f'Field `tool.hatch.build.targets.{self.plugin_name}.macos-max-compat` must be a boolean'
raise TypeError(message)

self.__macos_max_compat = macos_max_compat

return self.__macos_max_compat


class WheelBuilder(BuilderInterface):
"""
Expand Down Expand Up @@ -602,16 +615,27 @@ def get_best_matching_tag(self) -> str:
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
if sys.platform == 'darwin':
if 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}'

if self.config.macos_max_compat:
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}'
sdk_match = re.search(r'macosx_(\d+_\d+)', plat)
if sdk_match:
sdk_version_part = sdk_match.group(1)
if tuple(map(int, sdk_version_part.split('_'))) >= (11, 0):
tag_parts[2] = plat.replace(sdk_version_part, '10_16', 1)

return '-'.join(tag_parts)

Expand Down
4 changes: 4 additions & 0 deletions docs/history/hatchling.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),

## Unreleased

***Fixed:***

- Add `macos-max-compat` option to the `wheel` target that is enabled by default to support the latest version 22.0 of the `packaging` library

## [1.12.1](https://github.com/pypa/hatch/releases/tag/hatchling-v1.12.1) - 2022-12-31 ## {: #hatchling-v1.12.1 }

***Fixed:***
Expand Down
1 change: 1 addition & 0 deletions docs/plugins/builder/wheel.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ The builder plugin name is `wheel`.
| `shared-data` | | A mapping similar to the [forced inclusion](../../config/build.md#forced-inclusion) option corresponding to [data](https://peps.python.org/pep-0427/#the-data-directory) that will be installed globally in a given Python environment, usually under `#!python sys.prefix` |
| `extra-metadata` | | A mapping similar to the [forced inclusion](../../config/build.md#forced-inclusion) option corresponding to extra [metadata](https://peps.python.org/pep-0427/#the-dist-info-directory) that will be shipped in a directory named `extra_metadata` |
| `strict-naming` | `true` | Whether or not file names should contain the normalized version of the project name |
| `macos-max-compat` | `true` | Whether or not on macOS, when build hooks have set the `infer_tag` [build data](#build-data), the wheel name should signal broad support rather than specific versions for newer SDK versions.<br><br>Note: The default will become `false`, and this option eventually removed, sometime after consumers like pip start supporting these newer SDK versions. |

## Versions

Expand Down
126 changes: 118 additions & 8 deletions tests/backend/builders/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,28 @@ def test_target_overrides_global(self, isolation):
assert builder.config.strict_naming is True


class TestMacOSMaxCompat:
def test_default(self, isolation):
builder = WheelBuilder(str(isolation))

assert builder.config.macos_max_compat is builder.config.macos_max_compat is True

def test_correct(self, isolation):
config = {'tool': {'hatch': {'build': {'targets': {'wheel': {'macos-max-compat': False}}}}}}
builder = WheelBuilder(str(isolation), config=config)

assert builder.config.macos_max_compat is False

def test_not_boolean(self, isolation):
config = {'tool': {'hatch': {'build': {'targets': {'wheel': {'macos-max-compat': 9000}}}}}}
builder = WheelBuilder(str(isolation), config=config)

with pytest.raises(
TypeError, match='Field `tool.hatch.build.targets.wheel.macos-max-compat` must be a boolean'
):
_ = builder.config.macos_max_compat


class TestConstructEntryPointsFile:
def test_default(self, isolation):
config = {'project': {}}
Expand Down Expand Up @@ -1070,7 +1092,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
Expand Down Expand Up @@ -1152,7 +1174,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
Expand Down Expand Up @@ -1235,7 +1257,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
Expand Down Expand Up @@ -1318,7 +1340,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
},
Expand Down Expand Up @@ -1403,7 +1425,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
},
Expand Down Expand Up @@ -1484,7 +1506,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'src/my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
},
Expand Down Expand Up @@ -1760,7 +1782,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
Expand Down Expand Up @@ -2846,7 +2868,7 @@ def initialize(self, version, build_data):
'hatch': {
'version': {'path': 'my_app/__about__.py'},
'build': {
'targets': {'wheel': {'versions': ['standard']}},
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
'artifacts': ['my_app/lib.so'],
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
},
Expand Down Expand Up @@ -2888,3 +2910,91 @@ def initialize(self, version, build_data):
tag=expected_tag,
)
helpers.assert_files(extraction_directory, expected_files)

@pytest.mark.requires_macos
def test_macos_max_compat(self, hatch, helpers, temp_dir, config_file):
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():
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]
sdk_version_major, sdk_version_minor = tag_parts[2].split('_')[1:3]
if int(sdk_version_major) >= 11:
tag_parts[2] = tag_parts[2].replace(f'{sdk_version_major}_{sdk_version_minor}', '10_16', 1)

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)