File: //usr/share/dh-python/dhpython/build/plugin_pyproject.py
# Copyright © 2012-2020 Piotr Ożarowski <piotr@debian.org>
# © 2020 Scott Kitterman <scott@kitterman.com>
# © 2021 Stuart Prescott <stuart@debian.org>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
from pathlib import Path
import logging
import os.path as osp
import shutil
import sysconfig
try:
import tomli
except ModuleNotFoundError:
# Plugin still works, only needed for autodetection
pass
try:
from installer import install
from installer.destinations import SchemeDictionaryDestination
from installer.sources import WheelFile
except ModuleNotFoundError:
SchemeDictionaryDestination = WheelFile = install = None
from dhpython.build.base import Base, shell_command
log = logging.getLogger('dhpython')
class BuildSystem(Base):
DESCRIPTION = 'Generic PEP517 build system'
SUPPORTED_INTERPRETERS = {'python3', 'python{version}'}
REQUIRED_FILES = ['pyproject.toml']
OPTIONAL_FILES = {}
CLEAN_FILES = Base.CLEAN_FILES | {'build'}
def detect(self, context):
"""Return certainty level that this plugin describes the right build
system
This method uses cls.{REQUIRED}_FILES (pyroject.toml) only; any
other PEP517 compliant builder (such as the flit) builder should
indicate higher specificity than this plugin.
:return: 0 <= certainty <= 100
:rtype: int
"""
result = super().detect(context)
# Temporarily reduce the threshold while we're in beta
result -= 20
try:
with open('pyproject.toml', 'rb') as f:
pyproject = tomli.load(f)
if pyproject.get('build-system', {}).get('build-backend'):
result += 10
else:
# Not a PEP517 built package
result = 0
except NameError:
# No toml, no autdetection
result = 0
except FileNotFoundError:
# Not a PEP517 package
result = 0
if result > 100:
return 100
return result
def clean(self, context, args):
super().clean(context, args)
if osp.exists(args['interpreter'].binary()):
log.debug("removing '%s' (and everything under it)",
args['build_dir'])
osp.isdir(args['build_dir']) and shutil.rmtree(args['build_dir'])
return 0 # no need to invoke anything
def configure(self, context, args):
if install is None:
raise Exception("PEP517 plugin dependencies are not available. "
"Please Build-Depend on pybuild-plugin-pyproject.")
# No separate configure step
return 0
def build(self, context, args):
self.build_step1(context, args)
self.build_step2(context, args)
@shell_command
def build_step1(self, context, args):
""" build a wheel using the PEP517 builder defined by upstream """
log.info('Building wheel for %s with "build" module',
args['interpreter'])
args['ENV']['FLIT_NO_NETWORK'] = '1'
return ('{interpreter} -m build '
'--skip-dependency-check --no-isolation --wheel '
'--outdir ' + args['home_dir'] +
' {args}'
)
def build_step2(self, context, args):
""" unpack the wheel into pybuild's normal """
log.info('Unpacking wheel built for %s with "installer" module',
args['interpreter'])
# FIXME: setuptools would use scripts-X.Y; this could use usr/bin?
scripts = f'{args["build_dir"]}/scripts-{args["interpreter"].version}'
if osp.exists(scripts):
log.warning('Scripts directory already exists, skipping unpack. '
'Is the Python package being built twice?')
return
destination = SchemeDictionaryDestination(
{
'platlib': args['build_dir'],
'purelib': args['build_dir'],
'scripts': scripts,
#FIXME is this the right dest for data?
'data': args['build_dir']
},
interpreter=args['interpreter'].binary_dv,
script_kind='posix',
)
# FIXME this next step will unpack every single wheel file it finds
# which is probably ok since each wheel is built in a separate
# directory; but perhaps it should only accept the correctly named
# wheels that match the current interpreter?
# python-packaging has relevant utilities in
# - packaging/utils.py::parse_wheel_filename
# - packaging/tags.py (although it is current-interpreter-centric)
wheels = Path(args['home_dir']).glob('*.whl')
for wheel in wheels:
if wheel.name.startswith('UNKNOWN'):
raise Exception(f'UNKNOWN wheel found: {wheel.name}. Does '
'pyproject.toml specify a build-backend?')
with WheelFile.open(wheel) as source:
install(
source=source,
destination=destination,
additional_metadata={},
)
def install(self, context, args):
log.info('Copying package built for %s to destdir',
args['interpreter'])
try:
paths = sysconfig.get_paths(scheme='deb_system')
except KeyError:
# Debian hasn't patched sysconfig schemes until 3.10
# TODO: Introduce a version check once sysconfig is patched.
paths = sysconfig.get_paths(scheme='posix_prefix')
# start by copying the scripts
for script_dir in Path(args['build_dir']).glob('scripts-*'):
target_dir = args['destdir'] + paths['scripts']
log.debug('Copying scripts directory contents from %s -> %s',
script_dir, target_dir)
shutil.copytree(
script_dir,
target_dir,
dirs_exist_ok=True,
)
# then copy the modules
module_dir = args['build_dir']
target_dir = args['destdir'] + args['install_dir']
log.debug('Copying module contents from %s -> %s',
module_dir, target_dir)
shutil.copytree(
module_dir,
target_dir,
ignore=shutil.ignore_patterns('scripts-*'),
dirs_exist_ok=True,
)
@shell_command
def test(self, context, args):
return super().test(context, args)