Skip to content

Commit

Permalink
wip: PEP 660 backend support
Browse files Browse the repository at this point in the history
currently the contents for the {name}.pth file is hardcoded for the
psf/black project (because of course :P)
  • Loading branch information
ichard26 committed Nov 9, 2021
1 parent 8af23a4 commit bbe070e
Showing 1 changed file with 110 additions and 24 deletions.
134 changes: 110 additions & 24 deletions setuptools/build_meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand All @@ -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):
Expand Down Expand Up @@ -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)

This comment has been minimized.

Copy link
@ichard26

ichard26 Nov 9, 2021

Author Owner

This is honestly just a stopgap solution until I figure out how I'm going to query the importable base (see below for comment). Either this is going become a in-tree metadata build or result_directory will become a TemporaryDirectory.

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)

This comment has been minimized.

Copy link
@ichard26

ichard26 Nov 9, 2021

Author Owner

There's probably a less sketchy way of acquiring the normalized distribution name and version but I'm not quite sure what it'd be. I *think* the PathMetadata provider has an interface to query the distribution name and version, but I doubt they're normalized ...

dist = pkg_resources.DistInfoDistribution.from_filename(
dist_info_path, metadata=provider
)
wheel_name = f"{dist.project_name}-{dist.version}-ed.py3-none-any.whl"

This comment has been minimized.

Copy link
@ichard26

ichard26 Nov 9, 2021

Author Owner

I assume these tags are fine 🤞

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"

This comment has been minimized.

Copy link
@ichard26

ichard26 Nov 9, 2021

Author Owner

I honestly have no idea how I'm supposed to query the dist-info base (or the importable packages / modules parent dir). I can't run the dist-info command in-place and use its parent as the logic in _BACKEND._build_dist_info_metadata doesn't support a pre-populated result directory. I'd use this example from setuptools_pep660 but that codebase has the advantage of being implemented as a distutils Command which provides a great amount of metadata I've had to query manually here.

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
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit bbe070e

Please sign in to comment.