pypa-hatch/backend/hatchling/builders/sdist.py

272 lines
9.8 KiB
Python

import gzip
import os
import tarfile
import tempfile
from contextlib import closing
from copy import copy
from io import BytesIO
from time import time as get_current_timestamp
from ..metadata.utils import DEFAULT_METADATA_VERSION, get_core_metadata_constructors
from .plugin.interface import BuilderInterface
from .utils import get_reproducible_timestamp, normalize_archive_path, normalize_file_permissions, replace_file
class SdistArchive(object):
def __init__(self, name, reproducible):
"""
https://www.python.org/dev/peps/pep-0517/#source-distributions
"""
self.name = name
self.reproducible = reproducible
if reproducible:
self.timestamp = get_reproducible_timestamp()
else:
self.timestamp = None
raw_fd, self.path = tempfile.mkstemp(suffix='.tar.gz')
self.fd = os.fdopen(raw_fd, 'w+b')
self.gz = gzip.GzipFile(fileobj=self.fd, mode='wb', mtime=self.timestamp)
self.tf = tarfile.TarFile(fileobj=self.gz, mode='w', format=tarfile.PAX_FORMAT)
self.gettarinfo = lambda *args, **kwargs: self.normalize_tar_metadata(self.tf.gettarinfo(*args, **kwargs))
def create_file(self, contents, *relative_paths):
contents = contents.encode('utf-8')
tar_info = tarfile.TarInfo(normalize_archive_path(os.path.join(self.name, *relative_paths)))
tar_info.mtime = self.timestamp if self.reproducible else int(get_current_timestamp())
tar_info.size = len(contents)
with closing(BytesIO(contents)) as buffer:
self.tf.addfile(tar_info, buffer)
def normalize_tar_metadata(self, tar_info):
if not self.reproducible:
return tar_info
tar_info = copy(tar_info)
tar_info.uid = 0
tar_info.gid = 0
tar_info.uname = ''
tar_info.gname = ''
tar_info.mode = normalize_file_permissions(tar_info.mode)
tar_info.mtime = self.timestamp
return tar_info
def __getattr__(self, name):
attr = getattr(self.tf, name)
setattr(self, name, attr)
return attr
def __enter__(self):
return self
def __exit__(self, exc_type, exc_value, traceback):
self.tf.close()
self.gz.close()
self.fd.close()
class SdistBuilder(BuilderInterface):
"""
Build an archive of the source files
"""
PLUGIN_NAME = 'sdist'
def __init__(self, *args, **kwargs):
super(SdistBuilder, self).__init__(*args, **kwargs)
self.__core_metadata_constructor = None
self.__support_legacy = None
def get_version_api(self):
return {'standard': self.build_standard}
def get_default_versions(self):
return ['standard']
def clean(self, directory, versions):
for filename in os.listdir(directory):
if filename.endswith('.tar.gz'):
os.remove(os.path.join(directory, filename))
def build_standard(self, directory, **build_data):
found_packages = set()
with SdistArchive(self.project_id, self.reproducible) as archive:
for included_file in self.recurse_project_files():
if self.support_legacy:
possible_package, file_name = os.path.split(included_file.relative_path)
if file_name == '__init__.py':
found_packages.add(possible_package)
tar_info = archive.gettarinfo(
included_file.path,
arcname=normalize_archive_path(os.path.join(self.project_id, included_file.distribution_path)),
)
if tar_info.isfile():
with open(included_file.path, 'rb') as f:
archive.addfile(tar_info, f)
else: # no cov
# TODO: Investigate if this is necessary (for symlinks, etc.)
archive.addfile(tar_info)
archive.create_file(self.core_metadata_constructor(self.metadata), 'PKG-INFO')
if self.support_legacy:
archive.create_file(self.construct_setup_py_file(sorted(found_packages)), 'setup.py')
target = os.path.join(directory, '{}.tar.gz'.format(self.project_id))
replace_file(archive.path, target)
return target
def construct_setup_py_file(self, packages):
contents = '# -*- coding: utf-8 -*-\nfrom setuptools import setup\n\n'
contents += 'setup(\n'
contents += ' name={!r},\n'.format(self.metadata.core.name)
contents += ' version={!r},\n'.format(self.metadata.version)
if self.metadata.core.description:
contents += ' description={!r},\n'.format(self.metadata.core.description)
if self.metadata.core.readme:
contents += ' long_description={!r},\n'.format(self.metadata.core.readme)
authors_data = self.metadata.core.authors_data
if authors_data['name']:
contents += ' author={!r},\n'.format(', '.join(authors_data['name']))
if authors_data['email']:
contents += ' author_email={!r},\n'.format(', '.join(authors_data['email']))
maintainers_data = self.metadata.core.maintainers_data
if maintainers_data['name']:
contents += ' maintainer={!r},\n'.format(', '.join(maintainers_data['name']))
if maintainers_data['email']:
contents += ' maintainer_email={!r},\n'.format(', '.join(maintainers_data['email']))
if self.metadata.core.classifiers:
contents += ' classifiers=[\n'
for classifier in self.metadata.core.classifiers:
contents += ' {!r},\n'.format(classifier)
contents += ' ],\n'
if self.metadata.core.dependencies:
contents += ' install_requires=[\n'
for specifier in self.metadata.core.dependencies:
contents += ' {!r},\n'.format(specifier)
contents += ' ],\n'
if self.metadata.core.optional_dependencies:
contents += ' extras_require={\n'
for option, specifiers in self.metadata.core.optional_dependencies.items():
if not specifiers:
continue
contents += ' {!r}: [\n'.format(option)
for specifier in specifiers:
contents += ' {!r},\n'.format(specifier)
contents += ' ],\n'
contents += ' },\n'
if self.metadata.core.scripts or self.metadata.core.gui_scripts or self.metadata.core.entry_points:
contents += ' entry_points={\n'
if self.metadata.core.scripts:
contents += " 'console_scripts': [\n"
for name, object_ref in self.metadata.core.scripts.items():
contents += ' {!r},\n'.format('{} = {}'.format(name, object_ref))
contents += ' ],\n'
if self.metadata.core.gui_scripts:
contents += " 'gui_scripts': [\n"
for name, object_ref in self.metadata.core.gui_scripts.items():
contents += ' {!r},\n'.format('{} = {}'.format(name, object_ref))
contents += ' ],\n'
if self.metadata.core.entry_points:
for group, entry_points in self.metadata.core.entry_points.items():
contents += ' {!r}: [\n'.format(group)
for name, object_ref in entry_points.items():
contents += ' {!r},\n'.format('{} = {}'.format(name, object_ref))
contents += ' ],\n'
contents += ' },\n'
if packages:
contents += ' packages=[\n'
for package in packages:
contents += ' {!r},\n'.format(package.replace(os.path.sep, '.'))
contents += ' ],\n'
contents += ')\n'
return contents
def get_default_build_data(self):
build_data = {'artifacts': []}
if not self.include_path('pyproject.toml'):
build_data['artifacts'].append('/pyproject.toml')
readme_path = self.metadata.core.readme_path
if readme_path and not self.include_path(readme_path):
build_data['artifacts'].append('/{}'.format(readme_path))
license_files = self.metadata.core.license_files
if license_files:
for license_file in license_files:
if not self.include_path(license_file):
build_data['artifacts'].append('/{}'.format(license_file))
return build_data
@property
def core_metadata_constructor(self):
if self.__core_metadata_constructor is None:
core_metadata_version = self.target_config.get('core-metadata-version', DEFAULT_METADATA_VERSION)
if not isinstance(core_metadata_version, str):
raise TypeError(
'Field `tool.hatch.build.targets.{}.core-metadata-version` must be a string'.format(
self.PLUGIN_NAME
)
)
constructors = get_core_metadata_constructors()
if core_metadata_version not in constructors:
raise ValueError(
'Unknown metadata version `{}` for field `tool.hatch.build.targets.{}.core-metadata-version`. '
'Available: {}'.format(core_metadata_version, self.PLUGIN_NAME, ', '.join(sorted(constructors)))
)
self.__core_metadata_constructor = constructors[core_metadata_version]
return self.__core_metadata_constructor
@property
def support_legacy(self):
if self.__support_legacy is None:
self.__support_legacy = bool(self.target_config.get('support-legacy', False))
return self.__support_legacy