Skip to content

Commit d4c7c55

Browse files
authored
Add macos-max-compat option to the wheel target (#699)
1 parent 2583fc4 commit d4c7c55

File tree

4 files changed

+155
-16
lines changed

4 files changed

+155
-16
lines changed

backend/src/hatchling/builders/wheel.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ def __init__(self, *args: Any, **kwargs: Any) -> None:
167167
self.__shared_data: dict[str, str] | None = None
168168
self.__extra_metadata: dict[str, str] | None = None
169169
self.__strict_naming: bool | None = None
170+
self.__macos_max_compat: bool | None = None
170171

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

326327
return self.__strict_naming
327328

329+
@property
330+
def macos_max_compat(self) -> bool:
331+
if self.__macos_max_compat is None:
332+
macos_max_compat = self.target_config.get('macos-max-compat', True)
333+
if not isinstance(macos_max_compat, bool):
334+
message = f'Field `tool.hatch.build.targets.{self.plugin_name}.macos-max-compat` must be a boolean'
335+
raise TypeError(message)
336+
337+
self.__macos_max_compat = macos_max_compat
338+
339+
return self.__macos_max_compat
340+
328341

329342
class WheelBuilder(BuilderInterface):
330343
"""
@@ -602,16 +615,27 @@ def get_best_matching_tag(self) -> str:
602615
tag_parts = [tag.interpreter, tag.abi, tag.platform]
603616

604617
archflags = os.environ.get('ARCHFLAGS', '')
605-
if sys.platform == 'darwin' and archflags and sys.version_info[:2] >= (3, 8):
606-
import platform
607-
import re
618+
if sys.platform == 'darwin':
619+
if archflags and sys.version_info[:2] >= (3, 8):
620+
import platform
621+
import re
622+
623+
archs = re.findall(r'-arch (\S+)', archflags)
624+
if archs:
625+
plat = tag_parts[2]
626+
current_arch = platform.mac_ver()[2]
627+
new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0]
628+
tag_parts[2] = f'{plat[:plat.rfind(current_arch)]}{new_arch}'
629+
630+
if self.config.macos_max_compat:
631+
import re
608632

609-
archs = re.findall(r'-arch (\S+)', archflags)
610-
if archs:
611633
plat = tag_parts[2]
612-
current_arch = platform.mac_ver()[2]
613-
new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0]
614-
tag_parts[2] = f'{plat[:plat.rfind(current_arch)]}{new_arch}'
634+
sdk_match = re.search(r'macosx_(\d+_\d+)', plat)
635+
if sdk_match:
636+
sdk_version_part = sdk_match.group(1)
637+
if tuple(map(int, sdk_version_part.split('_'))) >= (11, 0):
638+
tag_parts[2] = plat.replace(sdk_version_part, '10_16', 1)
615639

616640
return '-'.join(tag_parts)
617641

docs/history/hatchling.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
88

99
## Unreleased
1010

11+
***Fixed:***
12+
13+
- 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
14+
1115
## [1.12.1](https://github.com/pypa/hatch/releases/tag/hatchling-v1.12.1) - 2022-12-31 ## {: #hatchling-v1.12.1 }
1216

1317
***Fixed:***

docs/plugins/builder/wheel.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ The builder plugin name is `wheel`.
2828
| `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` |
2929
| `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` |
3030
| `strict-naming` | `true` | Whether or not file names should contain the normalized version of the project name |
31+
| `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. |
3132

3233
## Versions
3334

tests/backend/builders/test_wheel.py

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,28 @@ def test_target_overrides_global(self, isolation):
379379
assert builder.config.strict_naming is True
380380

381381

382+
class TestMacOSMaxCompat:
383+
def test_default(self, isolation):
384+
builder = WheelBuilder(str(isolation))
385+
386+
assert builder.config.macos_max_compat is builder.config.macos_max_compat is True
387+
388+
def test_correct(self, isolation):
389+
config = {'tool': {'hatch': {'build': {'targets': {'wheel': {'macos-max-compat': False}}}}}}
390+
builder = WheelBuilder(str(isolation), config=config)
391+
392+
assert builder.config.macos_max_compat is False
393+
394+
def test_not_boolean(self, isolation):
395+
config = {'tool': {'hatch': {'build': {'targets': {'wheel': {'macos-max-compat': 9000}}}}}}
396+
builder = WheelBuilder(str(isolation), config=config)
397+
398+
with pytest.raises(
399+
TypeError, match='Field `tool.hatch.build.targets.wheel.macos-max-compat` must be a boolean'
400+
):
401+
_ = builder.config.macos_max_compat
402+
403+
382404
class TestConstructEntryPointsFile:
383405
def test_default(self, isolation):
384406
config = {'project': {}}
@@ -1070,7 +1092,7 @@ def initialize(self, version, build_data):
10701092
'hatch': {
10711093
'version': {'path': 'my_app/__about__.py'},
10721094
'build': {
1073-
'targets': {'wheel': {'versions': ['standard']}},
1095+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
10741096
'artifacts': ['my_app/lib.so'],
10751097
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
10761098
},
@@ -1152,7 +1174,7 @@ def initialize(self, version, build_data):
11521174
'hatch': {
11531175
'version': {'path': 'my_app/__about__.py'},
11541176
'build': {
1155-
'targets': {'wheel': {'versions': ['standard']}},
1177+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
11561178
'artifacts': ['my_app/lib.so'],
11571179
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
11581180
},
@@ -1235,7 +1257,7 @@ def initialize(self, version, build_data):
12351257
'hatch': {
12361258
'version': {'path': 'my_app/__about__.py'},
12371259
'build': {
1238-
'targets': {'wheel': {'versions': ['standard']}},
1260+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
12391261
'artifacts': ['my_app/lib.so'],
12401262
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
12411263
},
@@ -1318,7 +1340,7 @@ def initialize(self, version, build_data):
13181340
'hatch': {
13191341
'version': {'path': 'my_app/__about__.py'},
13201342
'build': {
1321-
'targets': {'wheel': {'versions': ['standard']}},
1343+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
13221344
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
13231345
},
13241346
},
@@ -1403,7 +1425,7 @@ def initialize(self, version, build_data):
14031425
'hatch': {
14041426
'version': {'path': 'my_app/__about__.py'},
14051427
'build': {
1406-
'targets': {'wheel': {'versions': ['standard']}},
1428+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
14071429
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
14081430
},
14091431
},
@@ -1484,7 +1506,7 @@ def initialize(self, version, build_data):
14841506
'hatch': {
14851507
'version': {'path': 'src/my_app/__about__.py'},
14861508
'build': {
1487-
'targets': {'wheel': {'versions': ['standard']}},
1509+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
14881510
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
14891511
},
14901512
},
@@ -1760,7 +1782,7 @@ def initialize(self, version, build_data):
17601782
'hatch': {
17611783
'version': {'path': 'my_app/__about__.py'},
17621784
'build': {
1763-
'targets': {'wheel': {'versions': ['standard']}},
1785+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
17641786
'artifacts': ['my_app/lib.so'],
17651787
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
17661788
},
@@ -2846,7 +2868,7 @@ def initialize(self, version, build_data):
28462868
'hatch': {
28472869
'version': {'path': 'my_app/__about__.py'},
28482870
'build': {
2849-
'targets': {'wheel': {'versions': ['standard']}},
2871+
'targets': {'wheel': {'versions': ['standard'], 'macos-max-compat': False}},
28502872
'artifacts': ['my_app/lib.so'],
28512873
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
28522874
},
@@ -2888,3 +2910,91 @@ def initialize(self, version, build_data):
28882910
tag=expected_tag,
28892911
)
28902912
helpers.assert_files(extraction_directory, expected_files)
2913+
2914+
@pytest.mark.requires_macos
2915+
def test_macos_max_compat(self, hatch, helpers, temp_dir, config_file):
2916+
config_file.model.template.plugins['default']['src-layout'] = False
2917+
config_file.save()
2918+
2919+
project_name = 'My.App'
2920+
2921+
with temp_dir.as_cwd():
2922+
result = hatch('new', project_name)
2923+
2924+
assert result.exit_code == 0, result.output
2925+
2926+
project_path = temp_dir / 'my-app'
2927+
2928+
vcs_ignore_file = project_path / '.gitignore'
2929+
vcs_ignore_file.write_text('*.pyc\n*.so\n*.h')
2930+
2931+
build_script = project_path / DEFAULT_BUILD_SCRIPT
2932+
build_script.write_text(
2933+
helpers.dedent(
2934+
"""
2935+
import pathlib
2936+
2937+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
2938+
2939+
class CustomHook(BuildHookInterface):
2940+
def initialize(self, version, build_data):
2941+
build_data['pure_python'] = False
2942+
build_data['infer_tag'] = True
2943+
2944+
pathlib.Path('my_app', 'lib.so').touch()
2945+
pathlib.Path('my_app', 'lib.h').touch()
2946+
"""
2947+
)
2948+
)
2949+
2950+
config = {
2951+
'project': {'name': project_name, 'requires-python': '>3', 'dynamic': ['version']},
2952+
'tool': {
2953+
'hatch': {
2954+
'version': {'path': 'my_app/__about__.py'},
2955+
'build': {
2956+
'targets': {'wheel': {'versions': ['standard']}},
2957+
'artifacts': ['my_app/lib.so'],
2958+
'hooks': {'custom': {'path': DEFAULT_BUILD_SCRIPT}},
2959+
},
2960+
},
2961+
},
2962+
}
2963+
builder = WheelBuilder(str(project_path), config=config)
2964+
2965+
build_path = project_path / 'dist'
2966+
build_path.mkdir()
2967+
2968+
with project_path.as_cwd():
2969+
artifacts = list(builder.build(str(build_path)))
2970+
2971+
assert len(artifacts) == 1
2972+
expected_artifact = artifacts[0]
2973+
2974+
build_artifacts = list(build_path.iterdir())
2975+
assert len(build_artifacts) == 1
2976+
assert expected_artifact == str(build_artifacts[0])
2977+
2978+
tag = next(sys_tags())
2979+
tag_parts = [tag.interpreter, tag.abi, tag.platform]
2980+
sdk_version_major, sdk_version_minor = tag_parts[2].split('_')[1:3]
2981+
if int(sdk_version_major) >= 11:
2982+
tag_parts[2] = tag_parts[2].replace(f'{sdk_version_major}_{sdk_version_minor}', '10_16', 1)
2983+
2984+
expected_tag = '-'.join(tag_parts)
2985+
assert expected_artifact == str(build_path / f'{builder.project_id}-{expected_tag}.whl')
2986+
2987+
extraction_directory = temp_dir / '_archive'
2988+
extraction_directory.mkdir()
2989+
2990+
with zipfile.ZipFile(str(expected_artifact), 'r') as zip_archive:
2991+
zip_archive.extractall(str(extraction_directory))
2992+
2993+
metadata_directory = f'{builder.project_id}.dist-info'
2994+
expected_files = helpers.get_template_files(
2995+
'wheel.standard_default_build_script_artifacts',
2996+
project_name,
2997+
metadata_directory=metadata_directory,
2998+
tag=expected_tag,
2999+
)
3000+
helpers.assert_files(extraction_directory, expected_files)

0 commit comments

Comments
 (0)