diff --git a/setuptools/build_meta.py b/setuptools/build_meta.py index d0ac613ba38..bc74dee031e 100644 --- a/setuptools/build_meta.py +++ b/setuptools/build_meta.py @@ -34,10 +34,16 @@ import contextlib import tempfile import warnings +import zipfile +import base64 +import textwrap +import hashlib +import csv import setuptools import distutils +import pkg_resources from pkg_resources import parse_requirements __all__ = ['get_requires_for_build_sdist', @@ -126,6 +132,27 @@ def suppress_known_deprecation(): yield +def _urlsafe_b64encode(data): + """urlsafe_b64encode without padding""" + return base64.urlsafe_b64encode(data).rstrip(b"=") + + +def _add_wheel_record(archive, dist_info): + """ + Add the wheel RECORD manifest. + """ + buffer = io.StringIO() + writer = csv.writer(buffer, delimiter=',', quotechar='"', lineterminator='\n') + for f in archive.namelist(): + data = archive.read(f) + size = len(data) + digest = hashlib.sha256(data).digest() + digest = "sha256=" + (_urlsafe_b64encode(digest).decode("ascii")) + writer.writerow((f, digest, size)) + record_path = os.path.join(dist_info, "RECORD") + archive.writestr(zipfile.ZipInfo(record_path), buffer.read()) + + class _BuildMetaBackend(object): def _fix_config(self, config_settings): @@ -146,6 +173,29 @@ def _get_build_requires(self, config_settings, requirements): return requirements + def _build_dist_info_metadata(self, result_directory): + sys.argv = sys.argv[:1] + [ + 'dist_info', '--egg-base', result_directory] + with no_install_setup_requires(): + self.run_setup() + + dist_info_directory = result_directory + while True: + dist_infos = [f for f in os.listdir(dist_info_directory) + if f.endswith('.dist-info')] + + if ( + len(dist_infos) == 0 and + len(_get_immediate_subdirectories(dist_info_directory)) == 1 + ): + + dist_info_directory = os.path.join( + dist_info_directory, os.listdir(dist_info_directory)[0]) + continue + + assert len(dist_infos) == 1 + return dist_infos[0], dist_info_directory + def run_setup(self, setup_script='setup.py'): # Note that we can reuse our build directory between calls # Correctness comes first, then optimization later @@ -166,39 +216,23 @@ def get_requires_for_build_sdist(self, config_settings=None): config_settings = self._fix_config(config_settings) return self._get_build_requires(config_settings, requirements=[]) + def get_requires_for_build_editable(self, config_settings=None): + config_settings = self._fix_config(config_settings) + return self._get_build_requires(config_settings, requirements=[]) + def prepare_metadata_for_build_wheel(self, metadata_directory, config_settings=None): - sys.argv = sys.argv[:1] + [ - 'dist_info', '--egg-base', metadata_directory] - with no_install_setup_requires(): - self.run_setup() - - dist_info_directory = metadata_directory - while True: - dist_infos = [f for f in os.listdir(dist_info_directory) - if f.endswith('.dist-info')] - - if ( - len(dist_infos) == 0 and - len(_get_immediate_subdirectories(dist_info_directory)) == 1 - ): - - dist_info_directory = os.path.join( - dist_info_directory, os.listdir(dist_info_directory)[0]) - continue - - assert len(dist_infos) == 1 - break - + dist_info, dist_info_directory = \ + self._build_dist_info_metadata(metadata_directory) # PEP 517 requires that the .dist-info directory be placed in the # metadata_directory. To comply, we MUST copy the directory to the root if dist_info_directory != metadata_directory: shutil.move( - os.path.join(dist_info_directory, dist_infos[0]), + os.path.join(dist_info_directory, dist_info), metadata_directory) shutil.rmtree(dist_info_directory, ignore_errors=True) - return dist_infos[0] + return dist_info def _build_with_temp_dir(self, setup_command, result_extension, result_directory, config_settings): @@ -235,6 +269,55 @@ def build_sdist(self, sdist_directory, config_settings=None): '.tar.gz', sdist_directory, config_settings) + def build_editable(self, wheel_directory, config_settings=None, + metadata_directory=None): + config_settings = self._fix_config(config_settings) + # TODO: using wheel_directory like this is probably a bad idea, fix it + dist_info, dist_info_directory = self._build_dist_info_metadata(wheel_directory) + dist_info_path = os.path.join(dist_info_directory, dist_info) + + sys.argv = [ + *sys.argv[:1], 'build_ext', '--inplace', + *config_settings['--global-option'] + ] + with no_install_setup_requires(): + self.run_setup() + # TODO: this is super sketchy, it's worth a cleanup + provider = pkg_resources.PathMetadata(dist_info_path, dist_info_path) + dist = pkg_resources.DistInfoDistribution.from_filename( + dist_info_path, metadata=provider + ) + wheel_name = f"{dist.project_name}-{dist.version}-ed.py3-none-any.whl" + wheel_path = os.path.join(wheel_directory, wheel_name) + + with zipfile.ZipFile( + wheel_path, "a", compression=zipfile.ZIP_DEFLATED + ) as archive: + # TODO: don't hardcode this obviously *somehow* + data = "/home/ichard26/programming/oss/black/src" + archive.writestr(zipfile.ZipInfo(f'{dist.project_name}.pth'), data) + + for file in sorted(os.listdir(dist_info_path)): + file = os.path.join(dist_info_path, file) + with open(file, "r", encoding="utf-8") as metadata: + zip_filename = os.path.relpath(file, dist_info_directory) + archive.writestr( + zipfile.ZipInfo(zip_filename), metadata.read() + ) + + archive.writestr( + zipfile.ZipInfo(os.path.join(dist_info, "WHEEL")), + textwrap.dedent(f"""\ + Wheel-Version: 1.0 + Generator: setuptools ({setuptools.__version__}) + Root-Is-Purelib: false + Tag: ed.py3-none-any + """) + ) + _add_wheel_record(archive, dist_info) + + return os.path.basename(wheel_path) + class _BuildMetaLegacyBackend(_BuildMetaBackend): """Compatibility backend for setuptools @@ -281,9 +364,12 @@ def run_setup(self, setup_script='setup.py'): get_requires_for_build_wheel = _BACKEND.get_requires_for_build_wheel get_requires_for_build_sdist = _BACKEND.get_requires_for_build_sdist +get_requires_for_build_editable = _BACKEND.get_requires_for_build_editable prepare_metadata_for_build_wheel = _BACKEND.prepare_metadata_for_build_wheel +prepare_metadata_for_build_editable = _BACKEND.prepare_metadata_for_build_wheel build_wheel = _BACKEND.build_wheel build_sdist = _BACKEND.build_sdist +build_editable = _BACKEND.build_editable # The legacy backend