From 9d7b19a2e3fdd663b80fc76702e9e88a61081c7a Mon Sep 17 00:00:00 2001 From: Ofek Lev Date: Sat, 17 Dec 2022 01:13:43 -0500 Subject: [PATCH] Acknowledge the `ARCHFLAGS` environment variable on macOS --- .github/workflows/test.yml | 6 +- backend/src/hatchling/builders/wheel.py | 28 ++++++-- docs/history/hatchling.md | 1 + tests/backend/builders/test_wheel.py | 93 ++++++++++++++++++++++++- 4 files changed, 119 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef16208f9..cea2a765c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,8 +24,8 @@ jobs: strategy: fail-fast: false matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11', 'pypy-3.9'] + os: [macos-latest] + python-version: ['3.11'] steps: - uses: actions/checkout@v3 @@ -48,7 +48,7 @@ jobs: run: hatch run lint:all - name: Run tests - run: hatch run full + run: hatch run full -vv - name: Disambiguate coverage filename run: mv .coverage ".coverage.${{ matrix.os }}.${{ matrix.python-version }}" diff --git a/backend/src/hatchling/builders/wheel.py b/backend/src/hatchling/builders/wheel.py index a8605c5fa..dcf22c4b4 100644 --- a/backend/src/hatchling/builders/wheel.py +++ b/backend/src/hatchling/builders/wheel.py @@ -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() @@ -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 = set(re.findall(r'-arch (\S+)', archflags)) + if archs: + plat = tag_parts[2] + current_arch = platform.mac_ver()[2] + new_arch = 'universal2' if 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, diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index b2b3f502e..e0fb8d660 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -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 diff --git a/tests/backend/builders/test_wheel.py b/tests/backend/builders/test_wheel.py index 23ac5dfdb..7667b394c 100644 --- a/tests/backend/builders/test_wheel.py +++ b/tests/backend/builders/test_wheel.py @@ -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', ) @@ -2796,3 +2796,94 @@ 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) + assert expected_artifact == str(build_path / f'{builder.project_id}-{"-".join(tag_parts)}.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=tag, + ) + helpers.assert_files(extraction_directory, expected_files)