| |
@@ -14,7 +14,6 @@
|
| |
import zipfile
|
| |
|
| |
from pyproject_requirements_txt import convert_requirements_txt
|
| |
- from pyproject_wheel import parse_config_settings_args
|
| |
|
| |
|
| |
# Some valid Python version specifiers are not supported.
|
| |
@@ -41,9 +40,23 @@
|
| |
# already echoed by the %pyproject_buildrequires macro
|
| |
sys.exit(0)
|
| |
|
| |
+ try:
|
| |
+ from pyproject_hooks import HookMissing
|
| |
+ except ImportError as e:
|
| |
+ print_err('Import error:', e)
|
| |
+ # already echoed by the %pyproject_buildrequires macro
|
| |
+ sys.exit(0)
|
| |
+
|
| |
# uses packaging, needs to be imported after packaging is verified to be present
|
| |
from pyproject_convert import convert
|
| |
|
| |
+ from _pyproject_hooks import (
|
| |
+ BackendHookError,
|
| |
+ BuildBackend,
|
| |
+ parse_config_settings_args,
|
| |
+ stderr_subprocess_runner,
|
| |
+ )
|
| |
+
|
| |
|
| |
def guess_reason_for_invalid_requirement(requirement_str):
|
| |
if ':' in requirement_str:
|
| |
@@ -201,79 +214,19 @@
|
| |
self.extend(requirements, **kwargs)
|
| |
|
| |
|
| |
- def toml_load(opened_binary_file):
|
| |
- try:
|
| |
- # tomllib is in the standard library since 3.11.0b1
|
| |
- import tomllib
|
| |
- except ImportError:
|
| |
- try:
|
| |
- import tomli as tomllib
|
| |
- except ImportError as e:
|
| |
- print_err('Import error:', e)
|
| |
- # already echoed by the %pyproject_buildrequires macro
|
| |
- sys.exit(0)
|
| |
- return tomllib.load(opened_binary_file)
|
| |
-
|
| |
-
|
| |
- def get_backend(requirements):
|
| |
- try:
|
| |
- f = open('pyproject.toml', 'rb')
|
| |
- except FileNotFoundError:
|
| |
- pyproject_data = {}
|
| |
- else:
|
| |
- with f:
|
| |
- pyproject_data = toml_load(f)
|
| |
-
|
| |
- buildsystem_data = pyproject_data.get('build-system', {})
|
| |
- requirements.extend(
|
| |
- buildsystem_data.get('requires', ()),
|
| |
- source='build-system.requires',
|
| |
- )
|
| |
-
|
| |
- backend_name = buildsystem_data.get('build-backend')
|
| |
- if not backend_name:
|
| |
- # https://www.python.org/dev/peps/pep-0517/:
|
| |
- # If the pyproject.toml file is absent, or the build-backend key is
|
| |
- # missing, the source tree is not using this specification, and tools
|
| |
- # should revert to the legacy behaviour of running setup.py
|
| |
- # (either directly, or by implicitly invoking the [following] backend).
|
| |
- # If setup.py is also not present program will mimick pip's behavior
|
| |
- # and end with an error.
|
| |
- if not os.path.exists('setup.py'):
|
| |
- raise FileNotFoundError('File "setup.py" not found for legacy project.')
|
| |
- backend_name = 'setuptools.build_meta:__legacy__'
|
| |
-
|
| |
- # Note: For projects without pyproject.toml, this was already echoed
|
| |
- # by the %pyproject_buildrequires macro, but this also handles cases
|
| |
- # with pyproject.toml without a specified build backend.
|
| |
- # If the default requirements change, also change them in the macro!
|
| |
- requirements.add('setuptools >= 40.8', source='default build backend')
|
| |
- requirements.add('wheel', source='default build backend')
|
| |
-
|
| |
+ def get_backend(requirements) -> BuildBackend:
|
| |
+ backend = BuildBackend.from_directory(default_runner=stderr_subprocess_runner)
|
| |
+ for require in backend.requires:
|
| |
+ requirements.add(require.name, source=require.reason)
|
| |
requirements.check(source='build backend')
|
| |
+ return backend
|
| |
|
| |
- backend_path = buildsystem_data.get('backend-path')
|
| |
- if backend_path:
|
| |
- # PEP 517 example shows the path as a list, but some projects don't follow that
|
| |
- if isinstance(backend_path, str):
|
| |
- backend_path = [backend_path]
|
| |
- sys.path = backend_path + sys.path
|
| |
-
|
| |
- module_name, _, object_name = backend_name.partition(":")
|
| |
- backend_module = importlib.import_module(module_name)
|
| |
-
|
| |
- if object_name:
|
| |
- return getattr(backend_module, object_name)
|
| |
-
|
| |
- return backend_module
|
| |
|
| |
-
|
| |
- def generate_build_requirements(backend, requirements):
|
| |
- get_requires = getattr(backend, 'get_requires_for_build_wheel', None)
|
| |
- if get_requires:
|
| |
- new_reqs = get_requires(config_settings=requirements.config_settings)
|
| |
- requirements.extend(new_reqs, source='get_requires_for_build_wheel')
|
| |
- requirements.check(source='get_requires_for_build_wheel')
|
| |
+ def generate_build_requirements(backend: BuildBackend, requirements):
|
| |
+ caller = backend.get_caller()
|
| |
+ new_reqs = caller.get_requires_for_build_wheel(requirements.config_settings)
|
| |
+ requirements.extend(new_reqs, source='get_requires_for_build_wheel')
|
| |
+ requirements.check(source='get_requires_for_build_wheel')
|
| |
|
| |
|
| |
def parse_metadata_file(metadata_file):
|
| |
@@ -295,17 +248,18 @@
|
| |
return package_name, requires
|
| |
|
| |
|
| |
- def generate_run_requirements_hook(backend, requirements):
|
| |
- hook_name = 'prepare_metadata_for_build_wheel'
|
| |
- prepare_metadata = getattr(backend, hook_name, None)
|
| |
- if not prepare_metadata:
|
| |
+ def generate_run_requirements_hook(backend: BuildBackend, requirements):
|
| |
+ try:
|
| |
+ dir_basename = backend.get_caller().prepare_metadata_for_build_wheel(
|
| |
+ '.', requirements.config_settings, _allow_fallback=False
|
| |
+ )
|
| |
+ except HookMissing:
|
| |
raise ValueError(
|
| |
'The build backend cannot provide build metadata '
|
| |
'(incl. runtime requirements) before build. '
|
| |
'Use the provisional -w flag to build the wheel and parse the metadata from it, '
|
| |
'or use the -R flag not to generate runtime dependencies.'
|
| |
- )
|
| |
- dir_basename = prepare_metadata('.', config_settings=requirements.config_settings)
|
| |
+ ) from None
|
| |
with open(dir_basename + '/METADATA') as metadata_file:
|
| |
name, requires = package_name_and_requires_from_metadata_file(metadata_file)
|
| |
for key, req in requires.items():
|
| |
@@ -328,17 +282,16 @@
|
| |
# Reuse the wheel from the previous round of %pyproject_buildrequires (if it exists)
|
| |
wheel = find_built_wheel(wheeldir)
|
| |
if not wheel:
|
| |
- import pyproject_wheel
|
| |
- returncode = pyproject_wheel.build_wheel(
|
| |
- wheeldir=wheeldir,
|
| |
- stdout=sys.stderr,
|
| |
- config_settings=requirements.config_settings,
|
| |
- )
|
| |
- if returncode != 0:
|
| |
- raise RuntimeError('Failed to build the wheel for %pyproject_buildrequires -w.')
|
| |
- wheel = find_built_wheel(wheeldir)
|
| |
- if not wheel:
|
| |
- raise RuntimeError('Cannot locate the built wheel for %pyproject_buildrequires -w.')
|
| |
+ os.makedirs(wheeldir, exist_ok=True)
|
| |
+ try:
|
| |
+ wheel = backend.get_caller().build_wheel(
|
| |
+ wheeldir, config_settings=requirements.config_settings
|
| |
+ )
|
| |
+ wheel = os.path.join(wheeldir, wheel)
|
| |
+ except Exception as exc:
|
| |
+ raise RuntimeError(
|
| |
+ 'Failed to build the wheel for %pyproject_buildrequires -w.'
|
| |
+ ) from exc
|
| |
|
| |
print_err(f'Reading metadata from {wheel}')
|
| |
with zipfile.ZipFile(wheel) as wheelfile:
|
| |
@@ -421,7 +374,7 @@
|
| |
*, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None,
|
| |
get_installed_version=importlib.metadata.version, # for dep injection
|
| |
generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True,
|
| |
- output, config_settings=None,
|
| |
+ config_settings=None,
|
| |
):
|
| |
"""Generate the BuildRequires for the project in the current directory
|
| |
|
| |
@@ -457,7 +410,7 @@
|
| |
except EndPass:
|
| |
return
|
| |
finally:
|
| |
- output.write_text(os.linesep.join(requirements.output_lines) + os.linesep)
|
| |
+ print(os.linesep.join(requirements.output_lines))
|
| |
|
| |
|
| |
def main(argv):
|
| |
@@ -484,9 +437,6 @@
|
| |
default="3", help=argparse.SUPPRESS,
|
| |
)
|
| |
parser.add_argument(
|
| |
- '--output', type=pathlib.Path, required=True, help=argparse.SUPPRESS,
|
| |
- )
|
| |
- parser.add_argument(
|
| |
'--wheeldir', metavar='PATH', default=None,
|
| |
help=argparse.SUPPRESS,
|
| |
)
|
| |
@@ -562,9 +512,11 @@
|
| |
python3_pkgversion=args.python3_pkgversion,
|
| |
requirement_files=args.requirement_files,
|
| |
use_build_system=args.use_build_system,
|
| |
- output=args.output,
|
| |
config_settings=parse_config_settings_args(args.config_settings),
|
| |
)
|
| |
+ except BackendHookError as exc:
|
| |
+ print_err(exc)
|
| |
+ exit(exc.returncode)
|
| |
except Exception:
|
| |
# Log the traceback explicitly (it's useful debug info)
|
| |
traceback.print_exc()
|
| |
(I know this is a big change. I'm happy to take any feedback. Feel free to look at this at your convenience. :)
Description
This isolates build backend calls in a subprocess instead of directly
importing the backends in pyproject_buildrequires.py.
It also removes use of the intermediary %_pyproject_buildrequires file.
It's now possible to redirect all subprocess output to stderr.
It also allows us more flexibly and consistently handles
config_settings between pyproject_buildrequires.py and
pyproject_wheel.py.
This is the same code that pip vendors to call PEP 517 hooks.
Includes: https://src.fedoraproject.org/rpms/pyproject-rpm-macros/pull-request/385
Bootstrapping
Yes, this adds a new dependency, but I think it's worth it for the reasons listed above. Fortunately, pyproject-hooks is small, stable, and has no runtime dependencies. pip and build already use this library to call PEP 517 hooks.
Take a look at https://src.fedoraproject.org/fork/gotmax23/rpms/python-pyproject-hooks/commits/bootstrap. Currently, this assumes that pyproject-hooks is built right after python-pip.
Regarding RHEL, you'd need python3-pyproject-hooks and python3.11-pyproject-hooks components in CRB or you'd need to vendor pyproject-hooks.
Code
I created a new _pyproject_hooks module to share some of the code that's used between pyproject_wheel and pyproject_buildrequires. The type annotations are compatible with Python 3.7 and above. The backend discovery code is based on the original code from pyproject_buildrequires.
The tests are changed minimally. They account for the fact that dependencies are echoed directly to stdout again and the new BackendHookError exceptions. I dropped the special workarounds for old pip versions 🎉.