From 156e2fc8fe454f5edcc2a7fbade17f3e92b88e23 Mon Sep 17 00:00:00 2001 From: Maxwell G Date: Sat, 20 May 2023 00:37:27 +0000 Subject: [PATCH] Allow passing config_settings to the build backend Resolves: https://bugzilla.redhat.com/2192581 --- macros.aaa-pyproject-srpm | 2 +- macros.pyproject | 6 ++-- pyproject-rpm-macros.spec | 6 +++- pyproject_buildrequires.py | 26 ++++++++++---- pyproject_buildrequires_testcases.yaml | 37 +++++++++++++++++++ pyproject_wheel.py | 48 +++++++++++++++++++++++-- test_pyproject_buildrequires.py | 1 + tests/config-settings-test.spec | 50 ++++++++++++++++++++++++++ tests/config_settings_test_backend.py | 31 ++++++++++++++++ tests/tests.yml | 3 ++ 10 files changed, 197 insertions(+), 13 deletions(-) create mode 100644 tests/config-settings-test.spec create mode 100644 tests/config_settings_test_backend.py diff --git a/macros.aaa-pyproject-srpm b/macros.aaa-pyproject-srpm index 9dab590..d845e96 100644 --- a/macros.aaa-pyproject-srpm +++ b/macros.aaa-pyproject-srpm @@ -4,4 +4,4 @@ # this macro will cause the package with the real macro to be installed. # When macros.pyproject is installed, it overrides this macro. # Note: This needs to maintain the same set of options as the real macro. -%pyproject_buildrequires(rRxtNwe:) echo 'pyproject-rpm-macros' && exit 0 +%pyproject_buildrequires(rRxtNwe:C:) echo 'pyproject-rpm-macros' && exit 0 diff --git a/macros.pyproject b/macros.pyproject index 9b29b73..5678610 100644 --- a/macros.pyproject +++ b/macros.pyproject @@ -26,11 +26,11 @@ # The value is read and used by the %%pytest and %%tox macros: %_set_pytest_addopts %global __pytest_addopts --ignore=%{_pyproject_builddir} -%pyproject_wheel() %{expand:\\\ +%pyproject_wheel(C:) %{expand:\\\ %_set_pytest_addopts mkdir -p "%{_pyproject_builddir}" CFLAGS="${CFLAGS:-${RPM_OPT_FLAGS}}" LDFLAGS="${LDFLAGS:-${RPM_LD_FLAGS}}" TMPDIR="%{_pyproject_builddir}" \\\ -%{__python3} -Bs %{_rpmconfigdir}/redhat/pyproject_wheel.py %{_pyproject_wheeldir} +%{__python3} -Bs %{_rpmconfigdir}/redhat/pyproject_wheel.py %{?**} %{_pyproject_wheeldir} } @@ -140,7 +140,7 @@ fi # Note: Keep the options in sync with this macro from macros.aaa-pyproject-srpm -%pyproject_buildrequires(rRxtNwe:) %{expand:\\\ +%pyproject_buildrequires(rRxtNwe:C:) %{expand:\\\ %_set_pytest_addopts # The _auto_set_build_flags feature does not do this in %%generate_buildrequires section, # but we want to get an environment consistent with %%build: diff --git a/pyproject-rpm-macros.spec b/pyproject-rpm-macros.spec index 1fca94a..59ff85a 100644 --- a/pyproject-rpm-macros.spec +++ b/pyproject-rpm-macros.spec @@ -13,7 +13,7 @@ License: MIT # 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.8.1 +Version: 1.9.0 Release: 1%{?dist} # Macro files @@ -161,6 +161,10 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856 %changelog +* Wed May 31 2023 Maxwell G - 1.9.0-1 +- Allow passing config_settings to the build backend. +- Resolves: rhbz#2192581 + * Wed May 31 2023 Miro HronĨok - 1.8.1-1 - On Python older than 3.11, use tomli instead of deprecated toml - Fix literal %% handling in %%{pyproject_files} on RPM 4.19 diff --git a/pyproject_buildrequires.py b/pyproject_buildrequires.py index e72874f..844fd38 100644 --- a/pyproject_buildrequires.py +++ b/pyproject_buildrequires.py @@ -14,6 +14,7 @@ import pathlib 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. @@ -67,7 +68,7 @@ def guess_reason_for_invalid_requirement(requirement_str): class Requirements: """Requirement gatherer. The macro will eventually print out output_lines.""" def __init__(self, get_installed_version, extras=None, - generate_extras=False, python3_pkgversion='3'): + generate_extras=False, python3_pkgversion='3', config_settings=None): self.get_installed_version = get_installed_version self.output_lines = [] self.extras = set() @@ -81,6 +82,7 @@ class Requirements: self.generate_extras = generate_extras self.python3_pkgversion = python3_pkgversion + self.config_settings = config_settings def add_extras(self, *extras): self.extras |= set(e.strip() for e in extras) @@ -269,7 +271,7 @@ def get_backend(requirements): def generate_build_requirements(backend, requirements): get_requires = getattr(backend, 'get_requires_for_build_wheel', None) if get_requires: - new_reqs = 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') @@ -303,7 +305,7 @@ def generate_run_requirements_hook(backend, requirements): '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('.') + dir_basename = prepare_metadata('.', config_settings=requirements.config_settings) 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(): @@ -327,7 +329,11 @@ def generate_run_requirements_wheel(backend, requirements, wheeldir): wheel = find_built_wheel(wheeldir) if not wheel: import pyproject_wheel - returncode = pyproject_wheel.build_wheel(wheeldir=wheeldir, stdout=sys.stderr) + 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) @@ -415,7 +421,7 @@ def generate_requires( *, 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, + output, config_settings=None, ): """Generate the BuildRequires for the project in the current directory @@ -426,7 +432,8 @@ def generate_requires( requirements = Requirements( get_installed_version, extras=extras or [], generate_extras=generate_extras, - python3_pkgversion=python3_pkgversion + python3_pkgversion=python3_pkgversion, + config_settings=config_settings, ) try: @@ -516,6 +523,12 @@ def main(argv): metavar='REQUIREMENTS.TXT', help=('Add buildrequires from file'), ) + parser.add_argument( + '-C', + dest='config_settings', + action='append', + help='Configuration settings to pass to the PEP 517 backend', + ) args = parser.parse_args(argv) @@ -550,6 +563,7 @@ def main(argv): 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 Exception: # Log the traceback explicitly (it's useful debug info) diff --git a/pyproject_buildrequires_testcases.yaml b/pyproject_buildrequires_testcases.yaml index 7a86330..aba6002 100644 --- a/pyproject_buildrequires_testcases.yaml +++ b/pyproject_buildrequires_testcases.yaml @@ -986,3 +986,40 @@ Self-referencing extras (maze): python3dist(rightdep) python3dist(startdep) result: 0 + +config_settings_control: + include_runtime: false + config_settings: + pyproject.toml: | + [build-system] + build-backend = "test_backend" + backend-path = ["."] + test_backend.py: | + def get_requires_for_build_wheel(config_settings=None): + if not (config_settings is None or isinstance(config_settings, dict)): + raise TypeError + if config_settings and "test-config-setting" in config_settings: + return ["test-config-setting"] + return ["test-no-config-setting"] + expected: | + python3dist(test-no-config-setting) + result: 0 + +config_settings: + include_runtime: false + config_settings: + test-config-setting: "" + pyproject.toml: | + [build-system] + build-backend = "test_backend" + backend-path = ["."] + test_backend.py: | + def get_requires_for_build_wheel(config_settings=None): + if not (config_settings is None or isinstance(config_settings, dict)): + raise TypeError + if config_settings and "test-config-setting" in config_settings: + return ["test-config-setting"] + return ["test-no-config-setting"] + expected: | + python3dist(test-config-setting) + result: 0 diff --git a/pyproject_wheel.py b/pyproject_wheel.py index 1936d9c..2aab1e9 100644 --- a/pyproject_wheel.py +++ b/pyproject_wheel.py @@ -1,8 +1,37 @@ +import argparse import sys import subprocess -def build_wheel(*, wheeldir, stdout=None): +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('=') + 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, value in config_settings.items(): + 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', @@ -15,11 +44,26 @@ def build_wheel(*, wheeldir, stdout=None): '--no-clean', '--progress-bar', 'off', '--verbose', + *get_config_settings_args(config_settings), '.', ) cp = subprocess.run(command, stdout=stdout) return cp.returncode +def parse_args(argv=None): + parser = argparse.ArgumentParser(prog='%pyproject_wheel') + parser.add_argument('wheeldir', help=argparse.SUPPRESS) + parser.add_argument( + '-C', + dest='config_settings', + action='append', + help='Configuration settings to pass to the PEP 517 backend', + ) + args = parser.parse_args(argv) + args.config_settings = parse_config_settings_args(args.config_settings) + return args + + if __name__ == '__main__': - sys.exit(build_wheel(wheeldir=sys.argv[1])) + sys.exit(build_wheel(**vars(parse_args()))) diff --git a/test_pyproject_buildrequires.py b/test_pyproject_buildrequires.py index 04a23a4..0fa07db 100644 --- a/test_pyproject_buildrequires.py +++ b/test_pyproject_buildrequires.py @@ -63,6 +63,7 @@ def test_data(case_name, capfd, tmp_path, monkeypatch): 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'] diff --git a/tests/config-settings-test.spec b/tests/config-settings-test.spec new file mode 100644 index 0000000..cc622ae --- /dev/null +++ b/tests/config-settings-test.spec @@ -0,0 +1,50 @@ +Name: config-settings-test +Version: 1.0.0 +Release: 1%{?dist} +Summary: Test config_settings support + +License: MIT +URL: ... +Source0: config_settings_test_backend.py + + +%description +%{summary}. + + +%prep +%autosetup -cT + +cp -p %{sources} . + +cat <<'EOF' >config_settings.py +""" +This is a test package +""" +EOF + +cat <<'EOF' >pyproject.toml +[build-system] +build-backend = "config_settings_test_backend" +backend-path = ["."] +requires = ["flit-core"] + +[project] +name = "config_settings" +version = "%{version}" +dynamic = ["description"] +EOF + + +%generate_buildrequires +%pyproject_buildrequires -C abc=123 -C xyz=456 -C--option-with-dashes=1 +%pyproject_buildrequires -C abc=123 -C xyz=456 -C--option-with-dashes=1 -w + + +%build +%pyproject_wheel -C abc=123 -C xyz=456 -C--option-with-dashes=1 + + +%changelog +* Fri May 19 2023 Maxwell G +- Initial package diff --git a/tests/config_settings_test_backend.py b/tests/config_settings_test_backend.py new file mode 100644 index 0000000..22d1fba --- /dev/null +++ b/tests/config_settings_test_backend.py @@ -0,0 +1,31 @@ +""" +This is a test backend for pyproject-rpm-macros' integration tests +It is not compliant with PEP 517 and omits some required hooks. +""" + +from flit_core import buildapi + +EXPECTED_CONFIG_SETTINGS = {"abc": "123", "xyz": "456", "--option-with-dashes": "1"} + + +def _verify_config_settings(config_settings): + print(f"config_settings={config_settings}") + if config_settings != EXPECTED_CONFIG_SETTINGS: + raise ValueError( + f"{config_settings!r} does not match expected {EXPECTED_CONFIG_SETTINGS!r}" + ) + + +def build_wheel(wheel_directory, config_settings=None, metadata_directory=None): + _verify_config_settings(config_settings) + return buildapi.build_wheel(wheel_directory, None, metadata_directory) + + +def get_requires_for_build_wheel(config_settings=None): + _verify_config_settings(config_settings) + return buildapi.get_requires_for_build_wheel(None) + + +def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): + _verify_config_settings(config_settings) + return buildapi.prepare_metadata_for_build_wheel(metadata_directory, None) diff --git a/tests/tests.yml b/tests/tests.yml index 132599d..fa90a45 100644 --- a/tests/tests.yml +++ b/tests/tests.yml @@ -94,6 +94,9 @@ - escape_percentages: dir: . run: ./mocktest.sh escape_percentages + - config-settings-test: + dir: . + run: ./mocktest.sh config-settings-test required_packages: - mock - rpmdevtools