build: write a bare minimal PEP 517 build backend
Drop our dependency on setuptools and write our own PEP 517 build backend. This turns out to be relatively straightforward — only ~100 lines, and avoids the workarounds we had to do to prevent setuptools from trying to write to the source tree. It's also dramatically faster. While we're at it, we can now fix a long-standing nag: we now have a proper place to make sure that `modules/checkout` gets called before building an sdist or a wheel. That means that you can run `tox` or `pip install .` in a freshly checked-out tree and expect correct results. We have to add a command-line interface to support wheel creation on RHEL8, where pip is not yet PEP517-aware. We don't implement any optional features of PEP 517 and we also don't support editable installs.
This commit is contained in:
parent
2cac96bc19
commit
10de075181
|
@ -47,7 +47,7 @@ distdir: $(DISTFILES)
|
|||
# Needed to ensure the tarball is correct for $(VERSION) override
|
||||
dist-hook: $(distdir)/src/cockpit/_version.py
|
||||
$(distdir)/src/cockpit/_version.py: FORCE
|
||||
$(srcdir)/tools/dist-setuptools '$(srcdir)' '$(distdir)'
|
||||
python3 '$(srcdir)'/src/build_backend.py --copy '$(srcdir)' '$(distdir)'
|
||||
@rm -f $(distdir)/src/cockpit/_version.py
|
||||
$(AM_V_GEN) echo "__version__ = '$(VERSION)'" > $@
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ srcdir="${0%/*}"
|
|||
|
||||
(
|
||||
cd "${srcdir}"
|
||||
modules/checkout
|
||||
echo "m4_define(VERSION_NUMBER, [$(git describe --tags --abbrev=0)+git])" > version.m4
|
||||
autoreconf -i --warnings obsolete
|
||||
)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
[build-system]
|
||||
requires = ["setuptools >= 39.2.0"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
requires = []
|
||||
backend-path = ['src']
|
||||
build-backend = 'build_backend'
|
||||
|
||||
[tool.mypy]
|
||||
mypy_path = 'src/cockpit'
|
||||
|
|
21
setup.cfg
21
setup.cfg
|
@ -1,21 +0,0 @@
|
|||
[metadata]
|
||||
name = cockpit
|
||||
version = attr: cockpit.__version__
|
||||
|
||||
[options]
|
||||
packages = find:
|
||||
package_dir =
|
||||
=src
|
||||
|
||||
[options.packages.find]
|
||||
where = src
|
||||
include = cockpit*
|
||||
|
||||
[options.entry_points]
|
||||
console_scripts =
|
||||
cockpit-bridge = cockpit.bridge:main
|
||||
cockpit-askpass = cockpit._vendor.ferny.interaction_client:main
|
||||
|
||||
[options.package_data]
|
||||
cockpit.data =
|
||||
*.html
|
|
@ -26,10 +26,8 @@ install-python:
|
|||
@# wheel-based installation with .dist-info.
|
||||
@# This needs to work on RHEL8 up through modern Fedora, offline, with
|
||||
@# system packages available to the build.
|
||||
@rm -rf tmp/pybuild
|
||||
'$(srcdir)'/tools/dist-setuptools '$(srcdir)' tmp/pybuild
|
||||
cd tmp/pybuild && python3 -c 'from setuptools import setup; setup()' bdist_wheel
|
||||
python3 -m pip install --no-index --force-reinstall --root='$(DESTDIR)/' --prefix='$(prefix)' tmp/pybuild/dist/*.whl
|
||||
python3 -m pip install --no-index --force-reinstall --root='$(DESTDIR)/' --prefix='$(prefix)' \
|
||||
"$$(python3 '$(srcdir)'/src/build_backend.py --wheel '$(srcdir)' tmp/wheel)"
|
||||
mkdir -p $(DESTDIR)$(libexecdir)
|
||||
mv -t $(DESTDIR)$(libexecdir) $(DESTDIR)$(bindir)/cockpit-askpass
|
||||
endif
|
||||
|
|
|
@ -0,0 +1,123 @@
|
|||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import zipfile
|
||||
|
||||
from typing import Dict, Iterable, Optional
|
||||
|
||||
from cockpit import __version__
|
||||
|
||||
PACKAGE = f'cockpit-{__version__}'
|
||||
TAG = 'py3-none-any'
|
||||
|
||||
|
||||
def find_sources(srcpkg: bool) -> Iterable[str]:
|
||||
try:
|
||||
subprocess.check_call(['modules/checkout'], stdout=2) # Needed for git builds...
|
||||
except FileNotFoundError: # ...but not present in tarball...
|
||||
pass # ...and not needed either, because...
|
||||
assert os.path.exists('src/cockpit/_vendor/ferny/__init__.py') # ...the code should exist there already.
|
||||
|
||||
if srcpkg:
|
||||
yield from {
|
||||
'pyproject.toml',
|
||||
'src/build_backend.py',
|
||||
}
|
||||
|
||||
for path, _dirs, files in os.walk('src', followlinks=True):
|
||||
if '__init__.py' in files:
|
||||
yield from [os.path.join(path, file) for file in files]
|
||||
|
||||
|
||||
def copy_sources(distdir: str) -> None:
|
||||
for source in find_sources(srcpkg=True):
|
||||
destination = os.path.join(distdir, source)
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
shutil.copy(source, destination)
|
||||
|
||||
|
||||
def build_sdist(sdist_directory: str,
|
||||
config_settings: Optional[Dict[str, object]] = None) -> str:
|
||||
del config_settings
|
||||
sdist_filename = f'{PACKAGE}.tar.gz'
|
||||
with tarfile.open(f'{sdist_directory}/{sdist_filename}', 'w:gz', dereference=True) as sdist:
|
||||
for filename in find_sources(srcpkg=True):
|
||||
sdist.add(filename, arcname=f'{PACKAGE}/{filename}', )
|
||||
return sdist_filename
|
||||
|
||||
|
||||
def build_wheel(wheel_directory: str,
|
||||
config_settings: Optional[Dict[str, object]] = None,
|
||||
metadata_directory: Optional[str] = None) -> str:
|
||||
del config_settings, metadata_directory
|
||||
wheel_filename = f'{PACKAGE}-{TAG}.whl'
|
||||
distinfo = {
|
||||
'WHEEL': [
|
||||
'Wheel-Version: 1.0',
|
||||
'Generator: cockpit build_backend',
|
||||
'Root-Is-Purelib: true',
|
||||
f'Tag: {TAG}',
|
||||
],
|
||||
'METADATA': [
|
||||
'Metadata-Version: 2.1',
|
||||
'Name: cockpit',
|
||||
f'Version: {__version__}',
|
||||
],
|
||||
'entry_points.txt': [
|
||||
'[console_scripts]',
|
||||
'cockpit-bridge = cockpit.bridge:main',
|
||||
'cockpit-askpass = cockpit._vendor.ferny.interaction_client:main',
|
||||
],
|
||||
}
|
||||
|
||||
with zipfile.ZipFile(f'{wheel_directory}/{wheel_filename}', 'w') as wheel:
|
||||
def write_distinfo(filename: str, lines: Iterable[str]) -> None:
|
||||
wheel.writestr(f'{PACKAGE}.dist-info/{filename}', ''.join(f'{line}\n' for line in lines))
|
||||
|
||||
def record_lines() -> Iterable[str]:
|
||||
for info in wheel.infolist():
|
||||
digest = hashlib.sha256(wheel.read(info.filename)).digest()
|
||||
b64_digest = base64.urlsafe_b64encode(digest).rstrip(b'=').decode('ascii')
|
||||
yield f'{info.filename},sha256={b64_digest},{info.file_size}'
|
||||
yield f'{PACKAGE}.dist-info/RECORD,,'
|
||||
|
||||
for filename in find_sources(srcpkg=False):
|
||||
wheel.write(filename, arcname=os.path.relpath(filename, start='src'))
|
||||
|
||||
for filename, lines in distinfo.items():
|
||||
write_distinfo(filename, lines)
|
||||
write_distinfo('RECORD', record_lines())
|
||||
|
||||
return wheel_filename
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group(required=True)
|
||||
group.add_argument('--copy', action='store_true')
|
||||
group.add_argument('--sdist', action='store_true')
|
||||
group.add_argument('--wheel', action='store_true')
|
||||
parser.add_argument('srcdir')
|
||||
parser.add_argument('destdir')
|
||||
args = parser.parse_args()
|
||||
|
||||
# We have to chdir() for PEP 517, so make sure dest is absolute
|
||||
destdir = os.path.abspath(args.destdir)
|
||||
os.chdir(args.srcdir)
|
||||
|
||||
os.makedirs(destdir, exist_ok=True)
|
||||
|
||||
if args.copy:
|
||||
copy_sources(destdir)
|
||||
elif args.sdist:
|
||||
print(os.path.join(destdir, build_sdist(destdir)))
|
||||
else:
|
||||
print(os.path.join(destdir, build_wheel(destdir)))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
|
@ -170,8 +170,6 @@ Requires: subscription-manager-cockpit
|
|||
%if %{cockpit_enable_python}
|
||||
BuildRequires: python3-devel
|
||||
BuildRequires: python3-pip
|
||||
BuildRequires: python3-setuptools
|
||||
BuildRequires: python3-wheel
|
||||
%if 0%{?rhel} == 0
|
||||
# All of these are only required for running pytest (which we only do on Fedora)
|
||||
BuildRequires: procps-ng
|
||||
|
|
|
@ -1,44 +0,0 @@
|
|||
#!/usr/bin/python3
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('srcdir')
|
||||
parser.add_argument('distdir')
|
||||
args = parser.parse_args()
|
||||
|
||||
# setuptools can't create an sdist without writing (at least) an .egg-info
|
||||
# directory to the source directory. That conflicts with automake's
|
||||
# desire that `make dist` should not modify the source directory.
|
||||
#
|
||||
# As a hack: we use the setuptools egg_info command (with its output
|
||||
# redirected to /tmp) to get a list of the files that we need to ship, and
|
||||
# copy those over manually.
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
subprocess.check_call([sys.executable, '-c', 'from setuptools import setup; setup();',
|
||||
'--quiet', 'egg_info', '--egg-base', tmpdir], cwd=args.srcdir)
|
||||
|
||||
# Collect the result
|
||||
# We need to filter out the /tmp/xyz/src/cockpit.egg-info bits,
|
||||
# which we can do by only taking the relative pathnames.
|
||||
with open(f'{tmpdir}/cockpit.egg-info/SOURCES.txt') as file:
|
||||
distfiles = [line.strip() for line in file if not os.path.isabs(line)]
|
||||
|
||||
# Copy the required source files into the named destination directory
|
||||
for filename in distfiles:
|
||||
source = f'{args.srcdir}/{filename}'
|
||||
destination = f'{args.distdir}/{filename}'
|
||||
if not os.path.exists(destination): # avoid re-copying e.g. AUTHORS
|
||||
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
||||
shutil.copy(source, destination)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Reference in New Issue