From c825dbf7157c8a04256be142979ef4eee682af4d Mon Sep 17 00:00:00 2001 From: Henry Schreiner Date: Wed, 5 Jun 2024 15:25:31 -0400 Subject: [PATCH] fix: macos deployment target selection Signed-off-by: Henry Schreiner --- backend/src/hatchling/builders/macos.py | 58 +++++++++++++++++++++++++ backend/src/hatchling/builders/wheel.py | 24 ++-------- tests/backend/utils/test_macos.py | 56 ++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 21 deletions(-) create mode 100644 backend/src/hatchling/builders/macos.py create mode 100644 tests/backend/utils/test_macos.py diff --git a/backend/src/hatchling/builders/macos.py b/backend/src/hatchling/builders/macos.py new file mode 100644 index 000000000..6857e3a20 --- /dev/null +++ b/backend/src/hatchling/builders/macos.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +import os +import platform +import re + +__all__ = ['process_macos_plat_tag'] + + +def process_macos_plat_tag(plat: str, /, *, compat: bool) -> str: + """ + Process the macOS platform tag. This will normalize the macOS version to + 10.16 if compat=True. If the MACOSX_DEPLOYMENT_TARGET environment variable + is set, then it will be used instead for the target version. If archflags + is set, then the archs will be respected, including a universal build. + """ + # Default to a native build + current_arch = platform.machine() + arm = current_arch == 'arm64' + + # Look for cross-compiles + archflags = os.environ.get('ARCHFLAGS', '') + if archflags and (archs := re.findall(r'-arch (\S+)', archflags)): + new_arch = 'universal2' if set(archs) == {'x86_64', 'arm64'} else archs[0] + arm = archs == ['arm64'] + plat = f'{plat[: plat.rfind(current_arch)]}{new_arch}' + + # Process macOS version + if sdk_match := re.search(r'macosx_(\d+_\d+)', plat): + macos_version = sdk_match.group(1) + target = os.environ.get('MACOSX_DEPLOYMENT_TARGET', None) + + try: + new_version = normalize_macos_version(target or macos_version, arm=arm, compat=compat) + except ValueError: + new_version = normalize_macos_version(macos_version, arm=arm, compat=compat) + + return plat.replace(macos_version, new_version, 1) + + return plat + + +def normalize_macos_version(version: str, *, arm: bool, compat: bool) -> str: + """ + Set minor version to 0 if major is 11+. Enforces 11+ if arm=True. 11+ is + converted to 10.16 if compat=True. Version is always returned in + "major_minor" format. + """ + version = version.replace('.', '_') + if '_' not in version: + version = f'{version}_0' + major, minor = (int(d) for d in version.split('_')[:2]) + major = max(major, 11) if arm else major + minor = 0 if major >= 11 else minor # noqa: PLR2004 + if compat and major >= 11: # noqa: PLR2004 + major = 10 + minor = 16 + return f'{major}_{minor}' diff --git a/backend/src/hatchling/builders/wheel.py b/backend/src/hatchling/builders/wheel.py index 4ebb2b067..69c60ee0e 100644 --- a/backend/src/hatchling/builders/wheel.py +++ b/backend/src/hatchling/builders/wheel.py @@ -783,28 +783,10 @@ def get_best_matching_tag(self) -> str: tag = next(iter(t for t in sys_tags() if 'manylinux' not in t.platform and 'musllinux' not in t.platform)) tag_parts = [tag.interpreter, tag.abi, tag.platform] - archflags = os.environ.get('ARCHFLAGS', '') 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 - - plat = tag_parts[2] - 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) + from backend.src.hatchling.builders.macos import process_macos_plat_tag + + tag_parts[2] = process_macos_plat_tag(tag_parts[2], compat=self.config.macos_max_compat) return '-'.join(tag_parts) diff --git a/tests/backend/utils/test_macos.py b/tests/backend/utils/test_macos.py new file mode 100644 index 000000000..3716ec556 --- /dev/null +++ b/tests/backend/utils/test_macos.py @@ -0,0 +1,56 @@ +from __future__ import annotations + +import platform + +import pytest + +from hatchling.builders.macos import normalize_macos_version, process_macos_plat_tag + + +@pytest.mark.parametrize( + ('plat', 'arch', 'compat', 'archflags', 'deptarget', 'expected'), + [ + ('macosx_10_9_x86_64', 'x86_64', False, '', '', 'macosx_10_9_x86_64'), + ('macosx_11_9_x86_64', 'x86_64', False, '', '', 'macosx_11_0_x86_64'), + ('macosx_12_0_x86_64', 'x86_64', True, '', '', 'macosx_10_16_x86_64'), + ('macosx_10_9_arm64', 'arm64', False, '', '', 'macosx_11_0_arm64'), + ('macosx_10_9_arm64', 'arm64', False, '-arch x86_64 -arch arm64', '', 'macosx_10_9_universal2'), + ('macosx_10_9_x86_64', 'x86_64', False, '-arch x86_64 -arch arm64', '', 'macosx_10_9_universal2'), + ('macosx_10_9_x86_64', 'x86_64', False, '-arch x86_64 -arch arm64', '12', 'macosx_12_0_universal2'), + ('macosx_10_9_x86_64', 'x86_64', False, '-arch arm64', '12.4', 'macosx_12_0_arm64'), + ('macosx_10_9_x86_64', 'x86_64', False, '-arch arm64', '10.12', 'macosx_11_0_arm64'), + ('macosx_10_9_x86_64', 'x86_64', True, '-arch arm64', '10.12', 'macosx_10_16_arm64'), + ], +) +def test_process_macos_plat_tag( + monkeypatch: pytest.MonkeyPatch, + *, + plat: str, + arch: str, + compat: bool, + archflags: str, + deptarget: str, + expected: str, +) -> None: + monkeypatch.setenv('ARCHFLAGS', archflags) + monkeypatch.setenv('MACOSX_DEPLOYMENT_TARGET', deptarget) + monkeypatch.setattr(platform, 'machine', lambda: arch) + + assert process_macos_plat_tag(plat, compat=compat) == expected + + +@pytest.mark.parametrize( + ('version', 'arm', 'compat', 'expected'), + [ + ('10_9', False, False, '10_9'), + ('10_9', False, True, '10_9'), + ('10_9', True, False, '11_0'), + ('10_9', True, True, '10_9'), + ('11_3', False, False, '11_0'), + ('12_3', True, False, '12_0'), + ('12_3', False, True, '10_16'), + ('12_3', True, True, '10_16'), + ], +) +def check_normalization(*, version: str, arm: bool, compat: bool, expected: str) -> None: + assert normalize_macos_version(version, arm=arm, compat=compat) == expected