Skip to content

Commit

Permalink
Add shared_data and shared_scripts build data for the wheel tar…
Browse files Browse the repository at this point in the history
…get (#1394)
  • Loading branch information
ofek authored Apr 16, 2024
1 parent a229af7 commit b198f97
Show file tree
Hide file tree
Showing 4 changed files with 215 additions and 6 deletions.
20 changes: 14 additions & 6 deletions backend/src/hatchling/builders/wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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]:
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

***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:***
Expand Down
2 changes: 2 additions & 0 deletions docs/plugins/builder/wheel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
195 changes: 195 additions & 0 deletions tests/backend/builders/test_wheel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand Down

0 comments on commit b198f97

Please sign in to comment.