diff --git a/backend/src/hatchling/builders/wheel.py b/backend/src/hatchling/builders/wheel.py index 1acac4b6a..139b716e5 100644 --- a/backend/src/hatchling/builders/wheel.py +++ b/backend/src/hatchling/builders/wheel.py @@ -585,25 +585,31 @@ def build_editable_explicit(self, directory: str, **build_data: Any) -> str: def write_data( self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any], extra_dependencies: Sequence[str] ) -> None: - self.add_shared_data(archive, records) - self.add_shared_scripts(archive, records) + self.add_shared_data(archive, records, build_data) + self.add_shared_scripts(archive, records, build_data) # Ensure metadata is written last, see https://peps.python.org/pep-0427/#recommended-archiver-features self.write_metadata(archive, records, build_data, extra_dependencies=extra_dependencies) - def add_shared_data(self, archive: WheelArchive, records: RecordFile) -> None: - for shared_file in self.recurse_explicit_files(self.config.shared_data): + def add_shared_data(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: + shared_data = dict(self.config.shared_data) + shared_data.update(normalize_inclusion_map(build_data['shared_data'], self.root)) + + for shared_file in self.recurse_explicit_files(shared_data): record = archive.add_shared_file(shared_file) records.write(record) - def add_shared_scripts(self, archive: WheelArchive, records: RecordFile) -> None: + def add_shared_scripts(self, archive: WheelArchive, records: RecordFile, build_data: dict[str, Any]) -> None: import re from io import BytesIO # https://packaging.python.org/en/latest/specifications/binary-distribution-format/#recommended-installer-features shebang = re.compile(rb'^#!.*(?:pythonw?|pypyw?)[0-9.]*(.*)', flags=re.DOTALL) - for shared_script in self.recurse_explicit_files(self.config.shared_scripts): + shared_scripts = dict(self.config.shared_scripts) + shared_scripts.update(normalize_inclusion_map(build_data['shared_scripts'], self.root)) + + for shared_script in self.recurse_explicit_files(shared_scripts): with open(shared_script.path, 'rb') as f: content = BytesIO() for line in f: @@ -784,6 +790,8 @@ def get_default_build_data(self) -> dict[str, Any]: # noqa: PLR6301 'dependencies': [], 'force_include_editable': {}, 'extra_metadata': {}, + 'shared_data': {}, + 'shared_scripts': {}, } def get_forced_inclusion_map(self, build_data: dict[str, Any]) -> dict[str, str]: diff --git a/docs/history/hatchling.md b/docs/history/hatchling.md index 246329d6e..f102fe1de 100644 --- a/docs/history/hatchling.md +++ b/docs/history/hatchling.md @@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), ## Unreleased +***Added:*** + +- Add `shared_data` and `shared_scripts` build data for the `wheel` target + ## [1.23.0](https://github.com/pypa/hatch/releases/tag/hatchling-v1.23.0) - 2024-04-14 ## {: #hatchling-v1.23.0 } ***Added:*** diff --git a/docs/plugins/builder/wheel.md b/docs/plugins/builder/wheel.md index 9ae3bc176..d27b182b6 100644 --- a/docs/plugins/builder/wheel.md +++ b/docs/plugins/builder/wheel.md @@ -56,5 +56,7 @@ This is data that can be modified by [build hooks](../build-hook/reference.md). | `infer_tag` | `#!python False` | When `tag` is not set, this may be enabled to use the one most specific to the platform, Python interpreter, and ABI | | `pure_python` | `#!python True` | Whether or not to write metadata indicating that the package does not contain any platform-specific files | | `dependencies` | | Extra [project dependencies](../../config/metadata.md#required) | +| `shared_data` | | Additional [`shared-data`](#options) entries, which take precedence in case of conflicts | +| `shared_scripts` | | Additional [`shared-scripts`](#options) entries, which take precedence in case of conflicts | | `extra_metadata` | | Additional [`extra-metadata`](#options) entries, which take precedence in case of conflicts | | `force_include_editable` | | Similar to the [`force_include` option](../build-hook/reference.md#build-data) but specifically for the `editable` [version](#versions) and takes precedence | diff --git a/tests/backend/builders/test_wheel.py b/tests/backend/builders/test_wheel.py index 476eb9104..cd3e38361 100644 --- a/tests/backend/builders/test_wheel.py +++ b/tests/backend/builders/test_wheel.py @@ -1921,6 +1921,81 @@ def test_default_shared_data(self, hatch, helpers, temp_dir, config_file): ) helpers.assert_files(extraction_directory, expected_files) + def test_default_shared_data_from_build_data(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' + + shared_data_path = temp_dir / 'data' + shared_data_path.ensure_dir_exists() + (shared_data_path / 'foo.txt').touch() + nested_data_path = shared_data_path / 'nested' + nested_data_path.ensure_dir_exists() + (nested_data_path / 'bar.txt').touch() + + 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['shared_data']['../data'] = '/' + """ + ) + ) + + config = { + 'project': {'name': project_name, 'requires-python': '>3', 'dynamic': ['version']}, + 'tool': { + 'hatch': { + 'version': {'path': 'my_app/__about__.py'}, + 'build': {'targets': {'wheel': {'versions': ['standard'], 'hooks': {'custom': {}}}}}, + }, + }, + } + builder = WheelBuilder(str(project_path), config=config) + + build_path = project_path / 'dist' + build_path.mkdir() + + with project_path.as_cwd(): + artifacts = list(builder.build(directory=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]) + + 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' + shared_data_directory = f'{builder.project_id}.data' + expected_files = helpers.get_template_files( + 'wheel.standard_default_shared_data', + project_name, + metadata_directory=metadata_directory, + shared_data_directory=shared_data_directory, + ) + helpers.assert_files(extraction_directory, expected_files) + def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file): config_file.model.template.plugins['default']['src-layout'] = False config_file.save() @@ -2026,6 +2101,126 @@ def test_default_shared_scripts(self, hatch, helpers, temp_dir, config_file): ) helpers.assert_files(extraction_directory, expected_files) + def test_default_shared_scripts_from_build_data(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' + + shared_data_path = temp_dir / 'data' + shared_data_path.ensure_dir_exists() + + binary_contents = os.urandom(1024) + (shared_data_path / 'binary').write_bytes(binary_contents) + (shared_data_path / 'other_script.sh').write_text( + helpers.dedent( + """ + + #!/bin/sh arg1 arg2 + echo "Hello, World!" + """ + ) + ) + (shared_data_path / 'python_script.sh').write_text( + helpers.dedent( + """ + + #!/usr/bin/env python3.11 arg1 arg2 + print("Hello, World!") + """ + ) + ) + (shared_data_path / 'pythonw_script.sh').write_text( + helpers.dedent( + """ + + #!/usr/bin/pythonw3.11 arg1 arg2 + print("Hello, World!") + """ + ) + ) + (shared_data_path / 'pypy_script.sh').write_text( + helpers.dedent( + """ + + #!/usr/bin/env pypy + print("Hello, World!") + """ + ) + ) + (shared_data_path / 'pypyw_script.sh').write_text( + helpers.dedent( + """ + + #!pypyw3.11 arg1 arg2 + print("Hello, World!") + """ + ) + ) + + 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['shared_scripts']['../data'] = '/' + """ + ) + ) + + config = { + 'project': {'name': project_name, 'requires-python': '>3', 'dynamic': ['version']}, + 'tool': { + 'hatch': { + 'version': {'path': 'my_app/__about__.py'}, + 'build': {'targets': {'wheel': {'versions': ['standard'], 'hooks': {'custom': {}}}}}, + }, + }, + } + builder = WheelBuilder(str(project_path), config=config) + + build_path = project_path / 'dist' + build_path.mkdir() + + with project_path.as_cwd(): + artifacts = list(builder.build(directory=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]) + + 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' + shared_data_directory = f'{builder.project_id}.data' + expected_files = helpers.get_template_files( + 'wheel.standard_default_shared_scripts', + project_name, + metadata_directory=metadata_directory, + shared_data_directory=shared_data_directory, + binary_contents=binary_contents, + ) + helpers.assert_files(extraction_directory, expected_files) + def test_default_extra_metadata(self, hatch, helpers, temp_dir, config_file): config_file.model.template.plugins['default']['src-layout'] = False config_file.save()