#401 Use pyproject_hooks to build wheel and call hooks
Opened a year ago by gotmax23. Modified 5 months ago
rpms/ gotmax23/pyproject-rpm-macros hooks  into  rawhide

file modified
-11
@@ -186,17 +186,6 @@ 

  Note that some projects don't use config settings at all

  and other projects may only accept config settings for one of the two steps.

  

- Note that the current implementation of the macros uses `pip` to build wheels.

- On some systems (notably on RHEL 9 with Python 3.9),

- `pip` is too old to understand `--config-settings`.

- Using the `-C` option for `%pyproject_wheel` (or `%pyproject_buildrequires -w`)

- is not supported there and will result to an error like:

- 

-     Usage:   

-       /usr/bin/python3 -m pip wheel [options] <requirement specifier> ...

-       ...

-     no such option: --config-settings

- 

  [config_settings]: https://peps.python.org/pep-0517/#config-settings

  

  

file added
+206
@@ -0,0 +1,206 @@ 

+ """

+ Code for working with pyproject backends

+ """

+ 

+ from __future__ import annotations

+ 

+ import dataclasses

+ import os

+ import subprocess

+ import sys

+ from collections.abc import Callable, Mapping, Sequence

+ from pathlib import Path

+ from typing import TYPE_CHECKING, Any, Optional, Union

+ 

+ from pyproject_hooks import BuildBackendHookCaller as _BuildBackendHookCaller

+ 

+ if TYPE_CHECKING:

+     StrPath = Union[str, os.PathLike[str]]

+     # This should match the interface of pyproject_hooks.default_subprocess_runner

+     RunnerFuncT = Callable[

+         [Sequence[StrPath], StrPath, Optional[Mapping[str, str]]], None

+     ]

+ 

+ 

+ @dataclasses.dataclass

+ class BackendRequirement:

+     """

+     Represents a backend requirement and the reason it was added

+     """

+ 

+     name: str

+     reason: str | None = None

+ 

+ 

+ class BackendHookError(Exception):

+     """

+     Unhandled backend hook failure

+     """

+ 

+     def __init__(self, hook_name: str, returncode: int) -> None:

+         self.hook_name = hook_name

+         self.returncode = returncode

+         super().__init__(f"Failed to call {self.hook_name} hook")

+ 

+     def __repr__(self) -> str:

+         hook_name, returncode = self.hook_name, self.returncode

+         return f"BuildBackendHookCaller({hook_name=}, {returncode=})"

+ 

+ 

+ class BuildBackendHookCaller(_BuildBackendHookCaller):

+     """

+     pyproject_hooks.BuildBackendHookCaller subclass that wraps backend errors

+     """

+ 

+     def _call_hook(self, hook_name, kwargs):

+         try:

+             return super()._call_hook(hook_name, kwargs)

+         except subprocess.CalledProcessError as exc:

+             raise BackendHookError(hook_name, exc.returncode) from None

+ 

+ 

+ DEFAULT_BUILD_BACKEND = "setuptools.build_meta:__legacy__"

+ DEFAULT_BUILD_REQUIRES = (

+     BackendRequirement("setuptools >= 40.8", reason="default build backend"),

+     BackendRequirement("wheel", reason="default build backend"),

+ )

+ 

+ 

+ def stderr_subprocess_runner(

+     cmd: Sequence[StrPath], cwd: StrPath, extra_environ: Mapping[str, str] | None = None

+ ) -> None:

+     """

+     Alternative to pyproject_hooks.default_subprocess_runner that redirects all

+     stdout to stderr.

+     """

+     extra_environ = extra_environ or {}

+     subprocess.run(

+         cmd,

+         stdout=sys.stderr,

+         cwd=cwd,

+         env={**os.environ, **extra_environ},

+         check=True,

+     )

+ 

+ 

+ class _Sentinel:

+     pass

+ 

+ 

+ @dataclasses.dataclass

+ class BuildBackend:

+     """

+     Represents a build backend

+     """

+ 

+     name: str

+     requires: list[BackendRequirement]

+     path: list[str] | None = None

+     directory: Path = Path()

+     default_runner: RunnerFuncT | None = None

+ 

+     @classmethod

+     def from_pyproject_data(

+         cls,

+         data: dict[str, Any],

+         directory: Path = Path(),

+         default_runner: RunnerFuncT | None = None,

+     ) -> BuildBackend:

+         """

+         Create a BuildBackend object based on a parsed pyproject.toml dictionary

+         """

+         buildsystem_data: dict[str, Any] = data.get("build-system", {})

+ 

+         requires: list[BackendRequirement] = [

+             BackendRequirement(i, reason="build-system.requires")

+             for i in buildsystem_data.get("requires", [])

+         ]

+ 

+         backend_name: str | None = 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 (directory / "setup.py").exists():

+                 raise FileNotFoundError('"setup.py" not found for legacy project.')

+             backend_name = DEFAULT_BUILD_BACKEND

+ 

+             # 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!

+             requires.extend(DEFAULT_BUILD_REQUIRES)

+ 

+         backend_path: list[str] = buildsystem_data.get("backend-path", [])

+         if isinstance(backend_path, str):

+             backend_path = [backend_path]

+ 

+         return cls(

+             name=backend_name,

+             requires=requires,

+             path=backend_path,

+             directory=directory,

+             default_runner=default_runner,

+         )

+ 

+     @classmethod

+     def from_directory(

+         cls, directory: Path = Path(), default_runner: RunnerFuncT | None = None

+     ) -> BuildBackend:

+         """

+         Create a BuildBackend object based on the pyproject.toml in `directory`.

+         """

+         if sys.version_info[:2] >= (3, 11):

+             import tomllib

+         else:

+             import tomli as tomllib

+ 

+         try:

+             f = (directory / "pyproject.toml").open("rb")

+         except FileNotFoundError:

+             pyproject_data = {}

+         else:

+             with f:

+                 pyproject_data = tomllib.load(f)

+         return cls.from_pyproject_data(

+             pyproject_data, directory, default_runner=default_runner

+         )

+ 

+     _DEFAULT_RUNNER = _Sentinel()

+ 

+     def get_caller(

+         self,

+         runner: RunnerFuncT | _Sentinel | None = _DEFAULT_RUNNER,

+     ) -> BuildBackendHookCaller:

+         """

+         Create a BuildBackendHookCaller object based on this BuildBackend

+         """

+         if runner is self._DEFAULT_RUNNER:

+             runner = self.default_runner

+ 

+         return BuildBackendHookCaller(self.directory, self.name, self.path, runner)

+ 

+ 

+ def parse_config_settings_args(config_settings):

+     """

+     Given a list of config `KEY=VALUE` formatted config settings,

+     return a dictionary that can be passed to PEP 517 hook functions.

+     """

+     if not config_settings:

+         return config_settings

+     new_config_settings = {}

+     for arg in config_settings:

+         key, _, value = arg.partition('=')

+         if key in new_config_settings:

+             if not isinstance(new_config_settings[key], list):

+                 # convert the existing value to a list

+                 new_config_settings[key] = [new_config_settings[key]]

+             new_config_settings[key].append(value)

+         else:

+             new_config_settings[key] = value

+     return new_config_settings

file modified
+2 -4
@@ -19,7 +19,6 @@ 

  %_pyproject_modules %{_builddir}/%{_pyproject_files_prefix}-pyproject-modules

  %_pyproject_ghost_distinfo %{_builddir}/%{_pyproject_files_prefix}-pyproject-ghost-distinfo

  %_pyproject_record %{_builddir}/%{_pyproject_files_prefix}-pyproject-record

- %_pyproject_buildrequires %{_builddir}/%{_pyproject_files_prefix}-pyproject-buildrequires

  

  # Avoid leaking %%{_pyproject_builddir} to pytest collection

  # https://bugzilla.redhat.com/show_bug.cgi?id=1935212
@@ -171,6 +170,7 @@ 

  echo 'python%{python3_pkgversion}-devel'

  echo 'python%{python3_pkgversion}dist(pip) >= 19'

  echo 'python%{python3_pkgversion}dist(packaging)'

+ echo 'python%{python3_pkgversion}dist(pyproject-hooks)'

  %{!-N:if [ -f pyproject.toml ]; then

    %["%{python3_pkgversion}" == "3"

      ? "echo '(python%{python3_pkgversion}dist(tomli) if python%{python3_pkgversion}-devel < 3.11)'"
@@ -191,10 +191,8 @@ 

  rm -rfv *.dist-info/ >&2

  if [ -f %{__python3} ]; then

    mkdir -p "%{_pyproject_builddir}"

-   echo -n > %{_pyproject_buildrequires}

    CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" TMPDIR="%{_pyproject_builddir}" \\\

-   RPM_TOXENV="%{toxenv}" HOSTNAME="rpmbuild" %{__python3} -Bs %{_rpmconfigdir}/redhat/pyproject_buildrequires.py %{?!_python_no_extras_requires:--generate-extras} --python3_pkgversion %{python3_pkgversion} --wheeldir %{_pyproject_wheeldir} --output %{_pyproject_buildrequires} %{?**} >&2

-   cat %{_pyproject_buildrequires}

+   RPM_TOXENV="%{toxenv}" HOSTNAME="rpmbuild" %{__python3} -Bs %{_rpmconfigdir}/redhat/pyproject_buildrequires.py %{?!_python_no_extras_requires:--generate-extras} --python3_pkgversion %{python3_pkgversion} --wheeldir %{_pyproject_wheeldir} %{?**}

  fi

  # Incomplete .dist-info dir might confuse importlib.metadata

  rm -rfv *.dist-info/ >&2

file modified
+8 -1
@@ -13,7 +13,7 @@ 

  #   Increment Y and reset Z when new macros or features are added

  #   Increment Z when this is a bugfix or a cosmetic change

  # Dropping support for EOL Fedoras is *not* considered a breaking change

- Version:        1.10.0

+ Version:        1.11.0

  Release:        1%{?dist}

  

  # Macro files
@@ -28,6 +28,7 @@ 

  Source105:      pyproject_construct_toxenv.py

  Source106:      pyproject_requirements_txt.py

  Source107:      pyproject_wheel.py

+ Source108:      _pyproject_hooks.py

  

  # Tests

  Source201:      test_pyproject_buildrequires.py
@@ -56,6 +57,7 @@ 

  BuildRequires:  python3dist(pyyaml)

  BuildRequires:  python3dist(packaging)

  BuildRequires:  python3dist(pip)

+ BuildRequires:  python3dist(pyproject-hooks)

  BuildRequires:  python3dist(setuptools)

  %if %{with tox_tests}

  BuildRequires:  python3dist(tox-current-env) >= 0.0.6
@@ -134,6 +136,7 @@ 

  install -pm 644 pyproject_construct_toxenv.py %{buildroot}%{_rpmconfigdir}/redhat/

  install -pm 644 pyproject_requirements_txt.py %{buildroot}%{_rpmconfigdir}/redhat/

  install -pm 644 pyproject_wheel.py %{buildroot}%{_rpmconfigdir}/redhat/

+ install -pm 644 _pyproject_hooks.py %{buildroot}%{_rpmconfigdir}/redhat/

  

  %check

  # assert the two signatures of %%pyproject_buildrequires match exactly
@@ -161,6 +164,7 @@ 

  %{_rpmconfigdir}/redhat/pyproject_construct_toxenv.py

  %{_rpmconfigdir}/redhat/pyproject_requirements_txt.py

  %{_rpmconfigdir}/redhat/pyproject_wheel.py

+ %{_rpmconfigdir}/redhat/_pyproject_hooks.py

  

  %doc README.md

  %license LICENSE
@@ -171,6 +175,9 @@ 

  

  

  %changelog

+ * Wed Dec 13 2023 Maxwell G <maxwell@gtmx.me> - 1.11.0-1

+ - Use pyproject_hooks to build wheel and call hooks

+ 

  * Wed Sep 13 2023 Python Maint <python-maint@redhat.com> - 1.10.0-1

  - Add %%_pyproject_check_import_allow_no_modules for automated environments

  - Fix handling of tox 4 provision without an explicit tox minversion

file modified
+46 -94
@@ -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()

@@ -85,6 +85,7 @@ 

    pyproject.toml: |

      [build-system]

      requires = ["pkg == 0.$.^.*"]

+     build-backend = "foo.build"

    except: ValueError

  

  Single value version with unsupported compatible operator:

file modified
+17 -57
@@ -1,68 +1,19 @@ 

  import argparse

+ import traceback

  import sys

- import subprocess

+ from pathlib import Path

  

+ from _pyproject_hooks import BuildBackend, BackendHookError, parse_config_settings_args

  

- def parse_config_settings_args(config_settings):

-     """

-     Given a list of config `KEY=VALUE` formatted config settings,

-     return a dictionary that can be passed to PEP 517 hook functions.

-     """

-     if not config_settings:

-         return config_settings

-     new_config_settings = {}

-     for arg in config_settings:

-         key, _, value = arg.partition('=')

-         if key in new_config_settings:

-             if not isinstance(new_config_settings[key], list):

-                 # convert the existing value to a list

-                 new_config_settings[key] = [new_config_settings[key]]

-             new_config_settings[key].append(value)

-         else:

-             new_config_settings[key] = value

-     return new_config_settings

  

- 

- def get_config_settings_args(config_settings):

-     """

-     Given a dictionary of PEP 517 backend config_settings,

-     yield --config-settings args that can be passed to pip's CLI

-     """

-     if not config_settings:

-         return

-     for key, values in config_settings.items():

-         if not isinstance(values, list):

-             values = [values]

-         for value in values:

-             if value == '':

-                 yield f'--config-settings={key}'

-             else:

-                 yield f'--config-settings={key}={value}'

- 

- 

- def build_wheel(*, wheeldir, stdout=None, config_settings=None):

-     command = (

-         sys.executable,

-         '-m', 'pip',

-         'wheel',

-         '--wheel-dir', wheeldir,

-         '--no-deps',

-         '--use-pep517',

-         '--no-build-isolation',

-         '--disable-pip-version-check',

-         '--no-clean',

-         '--progress-bar', 'off',

-         '--verbose',

-         *get_config_settings_args(config_settings),

-         '.',

-     )

-     cp = subprocess.run(command, stdout=stdout)

-     return cp.returncode

+ def build_wheel(*, wheeldir, config_settings=None):

+     wheeldir.mkdir(parents=True, exist_ok=True)

+     BuildBackend.from_directory().get_caller().build_wheel(wheeldir, config_settings)

  

  

  def parse_args(argv=None):

      parser = argparse.ArgumentParser(prog='%pyproject_wheel')

-     parser.add_argument('wheeldir', help=argparse.SUPPRESS)

+     parser.add_argument('wheeldir', help=argparse.SUPPRESS, type=Path)

      parser.add_argument(

          '-C',

          dest='config_settings',
@@ -75,4 +26,13 @@ 

  

  

  if __name__ == '__main__':

-     sys.exit(build_wheel(**vars(parse_args())))

+     try:

+         build_wheel(**vars(parse_args()))

+     except BackendHookError as exc:

+         print(exc, file=sys.stderr)

+         sys.exit(exc.returncode)

+     except Exception:

+         traceback.print_exc()

+         sys.exit(1)

+     else:

+         sys.exit(0)

@@ -1,5 +1,6 @@ 

  from pathlib import Path

  import importlib.metadata

+ import sys

  

  import packaging.version

  import pytest
@@ -7,6 +8,7 @@ 

  import yaml

  

  from pyproject_buildrequires import generate_requires

+ from _pyproject_hooks import BackendHookError

  

  SETUPTOOLS_VERSION = packaging.version.parse(setuptools.__version__)

  SETUPTOOLS_60 = SETUPTOOLS_VERSION >= packaging.version.parse('60')
@@ -25,7 +27,6 @@ 

      monkeypatch.chdir(cwd)

      wheeldir = cwd.joinpath('wheeldir')

      wheeldir.mkdir()

-     output = tmp_path.joinpath('output.txt')

  

      if case.get('xfail'):

          pytest.xfail(case.get('xfail'))
@@ -62,11 +63,14 @@ 

              generate_extras=case.get('generate_extras', False),

              requirement_files=requirement_files,

              use_build_system=use_build_system,

-             output=output,

              config_settings=case.get('config_settings'),

          )

      except SystemExit as e:

          assert e.code == case['result']

+     # Mirror the way the actual code handles BackendHookError

+     except BackendHookError as e:

+         print(e, file=sys.stderr)

+         assert e.returncode == case['result']

      except Exception as e:

          if 'except' not in case:

              raise
@@ -78,8 +82,7 @@ 

          # if we ever need to do that, we can remove the check or change it:

          assert 'expected' in case or 'stderr_contains' in case

  

-         out, err = capfd.readouterr()

-         dependencies = output.read_text()

+         dependencies, err = capfd.readouterr()

  

          if 'expected' in case:

              expected = case['expected']

@@ -27,7 +27,7 @@ 

  [build-system]

  build-backend = "config_settings_test_backend"

  backend-path = ["."]

- requires = ["flit-core", "packaging", "pip"]

+ requires = ["flit-core"]

  

  [project]

  name = "config_settings"
@@ -38,11 +38,11 @@ 

  

  %generate_buildrequires

  %pyproject_buildrequires -C abc=123 -C xyz=456 -C--option-with-dashes=1 -C--option-with-dashes=2

- %{!?el9:%pyproject_buildrequires -C abc=123 -C xyz=456 -C--option-with-dashes=1 -C--option-with-dashes=2 -w}

+ %pyproject_buildrequires -C abc=123 -C xyz=456 -C--option-with-dashes=1 -C--option-with-dashes=2 -w

  

  

  %build

- %{!?el9:%pyproject_wheel -C abc=123 -C xyz=456 -C--option-with-dashes=1 -C--option-with-dashes=2}

+ %pyproject_wheel -C abc=123 -C xyz=456 -C--option-with-dashes=1 -C--option-with-dashes=2

  

  

  %changelog

@@ -4,17 +4,8 @@ 

  """

  

  from flit_core import buildapi

- from packaging.version import parse

- from pip import __version__ as pip_version

  

  EXPECTED_CONFIG_SETTINGS = [{"abc": "123", "xyz": "456", "--option-with-dashes": ["1", "2"]}]

- # Older pip did not accept multiple values,

- # but we might backport that later,

- # hence we accept it both ways with older pips

- if parse(pip_version) < parse("23.1"):

-     EXPECTED_CONFIG_SETTINGS.append(

-         EXPECTED_CONFIG_SETTINGS[0] | {"--option-with-dashes": "2"}

-     )

  

  

  def _verify_config_settings(config_settings):

(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 🎉.

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/ead19bb00efd4cab9ba2231aec56b645

5 new commits added

  • Use pyproject_hooks to build wheel and call hooks
  • Accept multiple values for the same config settings
  • document config_settings support
  • buildrequires: make -C and -N mutually exclusive
  • Allow passing config_settings to the build backend
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/03848c47a04f4714979f0a385bdb98b8

5 new commits added

  • Use pyproject_hooks to build wheel and call hooks
  • Accept multiple values for the same config settings
  • document config_settings support
  • buildrequires: make -C and -N mutually exclusive
  • Allow passing config_settings to the build backend
a year ago

5 new commits added

  • Use pyproject_hooks to build wheel and call hooks
  • Accept multiple values for the same config settings
  • document config_settings support
  • buildrequires: make -C and -N mutually exclusive
  • Allow passing config_settings to the build backend
a year ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/15df58758dee48eab764c07035a910e9

5 new commits added

  • Use pyproject_hooks to build wheel and call hooks
  • Accept multiple values for the same config settings
  • document config_settings support
  • buildrequires: make -C and -N mutually exclusive
  • Allow passing config_settings to the build backend
a year ago

Build succeeded.
https://fedora.softwarefactory-project.io/zuul/buildset/3bb26b3c9877435abb10d4fdbd7b4ea0

rebased onto 81781b5913c5444ea598be3ea399a249356cd6e0

a year ago

Awesome!

I'll ship the config setting feature and then I'll need a break from pyproject macros for a while, but happy to revisit this later.

Awesome!

I'll ship the config setting feature and then I'll need a break from pyproject macros for a while, but happy to revisit this later.

Build succeeded.
https://fedora.softwarefactory-project.io/zuul/buildset/50982e82c336413097e37d8565ed965c

rebased onto df4aacbd48b845d151425c1bf9f497cc5f744cb1

11 months ago

Build succeeded.
https://fedora.softwarefactory-project.io/zuul/buildset/2b526d0fb648469d919d683b4541014d

(I still know about this one and I still plan to eventually land it, but I am swamped with more urgent tasks.)

rebased onto b3229cf5484f33c952d1d6a265efa29b83807879

8 months ago

Build succeeded.
https://fedora.softwarefactory-project.io/zuul/buildset/677a7d62fc20493980110e09fe9037af

1 new commit added

  • move parse_config_settings_args() to _pyproject_hooks
8 months ago

Build succeeded.
https://fedora.softwarefactory-project.io/zuul/buildset/60d31a6e5bef481f8155ed223eda5a3e

rebased onto f7d0cd3

5 months ago

Build failed. More information on how to proceed and troubleshoot errors available at https://fedoraproject.org/wiki/Zuul-based-ci
https://fedora.softwarefactory-project.io/zuul/buildset/87d400982aeb4d0fa8f75b2f33375328