Blame pyproject_buildrequires.py

40f6765
import glob
40f6765
import io
fda0a23
import os
fdf5116
import sys
390b971
import importlib.metadata
50645e1
import argparse
50645e1
import traceback
7e1a8fd
import json
50645e1
import subprocess
50645e1
import re
7ba1a33
import tempfile
bc156c4
import email.parser
d6ad9a7
import pathlib
40f6765
import zipfile
aeb21f6
aeb21f6
from pyproject_requirements_txt import convert_requirements_txt
156e2fc
from pyproject_wheel import parse_config_settings_args
50645e1
50645e1
50645e1
# Some valid Python version specifiers are not supported.
ddaf2e9
# Allow only the forms we know we can handle.
ddaf2e9
VERSION_RE = re.compile(r'[a-zA-Z0-9.-]+(\.\*)?')
50645e1
50464a4
50645e1
class EndPass(Exception):
50645e1
    """End current pass of generating requirements"""
fdf5116
50464a4
ddaf2e9
# nb: we don't use functools.partial to be able to use pytest's capsys
ddaf2e9
# see https://github.com/pytest-dev/pytest/issues/8900
ddaf2e9
def print_err(*args, **kwargs):
ddaf2e9
    kwargs.setdefault('file', sys.stderr)
ddaf2e9
    print(*args, **kwargs)
ddaf2e9
ddaf2e9
fdf5116
try:
fdf5116
    from packaging.requirements import Requirement, InvalidRequirement
2abcad9
    from packaging.utils import canonicalize_name
50645e1
except ImportError as e:
50645e1
    print_err('Import error:', e)
fdf5116
    # already echoed by the %pyproject_buildrequires macro
fdf5116
    sys.exit(0)
fdf5116
cb38f21
# uses packaging, needs to be imported after packaging is verified to be present
cb38f21
from pyproject_convert import convert
cb38f21
fdf5116
f8a3343
def guess_reason_for_invalid_requirement(requirement_str):
f8a3343
    if ':' in requirement_str:
aeb21f6
        message = (
f8a3343
            'It might be an URL. '
f8a3343
            '%pyproject_buildrequires cannot handle all URL-based requirements. '
f8a3343
            'Add PackageName@ (see PEP 508) to the URL to at least require any version of PackageName.'
f8a3343
        )
aeb21f6
        if '@' in requirement_str:
aeb21f6
            message += ' (but note that URLs might not work well with other features)'
aeb21f6
        return message
f8a3343
    if '/' in requirement_str:
f8a3343
        return (
f8a3343
            'It might be a local path. '
f8a3343
            '%pyproject_buildrequires cannot handle local paths as requirements. '
f8a3343
            'Use an URL with PackageName@ (see PEP 508) to at least require any version of PackageName.'
f8a3343
        )
f8a3343
    # No more ideas
f8a3343
    return None
f8a3343
f8a3343
50645e1
class Requirements:
4569036
    """Requirement gatherer. The macro will eventually print out output_lines."""
2ecbed7
    def __init__(self, get_installed_version, extras=None,
156e2fc
                 generate_extras=False, python3_pkgversion='3', config_settings=None):
d262d90
        self.get_installed_version = get_installed_version
4569036
        self.output_lines = []
9f3eea2
        self.extras = set()
50645e1
6a8d86e
        if extras:
2ecbed7
            for extra in extras:
2ecbed7
                self.add_extras(*extra.split(','))
d6e6bb7
50645e1
        self.missing_requirements = False
bd78901
        self.ignored_alien_requirements = []
50645e1
a613e17
        self.generate_extras = generate_extras
5561755
        self.python3_pkgversion = python3_pkgversion
156e2fc
        self.config_settings = config_settings
5561755
9f3eea2
    def add_extras(self, *extras):
9f3eea2
        self.extras |= set(e.strip() for e in extras)
9f3eea2
9f3eea2
    @property
9f3eea2
    def marker_envs(self):
9f3eea2
        if self.extras:
9f3eea2
            return [{'extra': e} for e in sorted(self.extras)]
9f3eea2
        return [{'extra': ''}]
9f3eea2
dd0f198
    def evaluate_all_environments(self, requirement):
6a8d86e
        for marker_env in self.marker_envs:
6a8d86e
            if requirement.marker.evaluate(environment=marker_env):
6a8d86e
                return True
6a8d86e
        return False
6a8d86e
bd78901
    def add(self, requirement_str, *, package_name=None, source=None):
50645e1
        """Output a Python-style requirement string as RPM dep"""
50645e1
        print_err(f'Handling {requirement_str} from {source}')
fdf5116
fdf5116
        try:
50645e1
            requirement = Requirement(requirement_str)
f8a3343
        except InvalidRequirement:
aeb21f6
            hint = guess_reason_for_invalid_requirement(requirement_str)
aeb21f6
            message = f'Requirement {requirement_str!r} from {source} is invalid.'
aeb21f6
            if hint:
aeb21f6
                message += f' Hint: {hint}'
aeb21f6
            raise ValueError(message)
f8a3343
f8a3343
        if requirement.url:
50645e1
            print_err(
f8a3343
                f'WARNING: Simplifying {requirement_str!r} to {requirement.name!r}.'
50645e1
            )
50645e1
50645e1
        name = canonicalize_name(requirement.name)
50464a4
        if (requirement.marker is not None and
dd0f198
                not self.evaluate_all_environments(requirement)):
50645e1
            print_err(f'Ignoring alien requirement:', requirement_str)
bd78901
            self.ignored_alien_requirements.append(requirement_str)
bd78901
            return
bd78901
bd78901
        # Handle self-referencing requirements
bd78901
        if package_name and canonicalize_name(package_name) == name:
bd78901
            # Self-referential extras need to be handled specially
bd78901
            if requirement.extras:
bd78901
                if not (requirement.extras <= self.extras):  # only handle it if needed
bd78901
                    # let all further requirements know we want those extras
bd78901
                    self.add_extras(*requirement.extras)
bd78901
                    # re-add all of the alien requirements ignored in the past
bd78901
                    # they might no longer be alien now
bd78901
                    self.readd_ignored_alien_requirements(package_name=package_name)
bd78901
            else:
bd78901
                print_err(f'Ignoring self-referential requirement without extras:', requirement_str)
50645e1
            return
50645e1
27e23c1
        # We need to always accept pre-releases as satisfying the requirement
27e23c1
        # Otherwise e.g. installed cffi version 1.15.0rc2 won't even satisfy the requirement for "cffi"
27e23c1
        # https://bugzilla.redhat.com/show_bug.cgi?id=2014639#c3
27e23c1
        requirement.specifier.prereleases = True
27e23c1
d262d90
        try:
a613e17
            # TODO: check if requirements with extras are satisfied
d262d90
            installed = self.get_installed_version(requirement.name)
390b971
        except importlib.metadata.PackageNotFoundError:
d262d90
            print_err(f'Requirement not satisfied: {requirement_str}')
d262d90
            installed = None
50645e1
        if installed and installed in requirement.specifier:
50645e1
            print_err(f'Requirement satisfied: {requirement_str}')
50645e1
            print_err(f'   (installed: {requirement.name} {installed})')
a613e17
            if requirement.extras:
a613e17
                print_err(f'   (extras are currently not checked)')
50645e1
        else:
50645e1
            self.missing_requirements = True
50645e1
a613e17
        if self.generate_extras:
99ed463
            extra_names = [f'{name}[{extra.lower()}]' for extra in sorted(requirement.extras)]
50645e1
        else:
a613e17
            extra_names = []
a613e17
a613e17
        for name in [name] + extra_names:
a613e17
            together = []
a613e17
            for specifier in sorted(
a613e17
                requirement.specifier,
a613e17
                key=lambda s: (s.operator, s.version),
a613e17
            ):
a613e17
                if not VERSION_RE.fullmatch(str(specifier.version)):
a613e17
                    raise ValueError(
a613e17
                        f'Unknown character in version: {specifier.version}. '
f8a3343
                        + '(This might be a bug in pyproject-rpm-macros.)',
a613e17
                    )
cb38f21
                together.append(convert(python3dist(name, python3_pkgversion=self.python3_pkgversion),
2abcad9
                                        specifier.operator, specifier.version))
a613e17
            if len(together) == 0:
4569036
                dep = python3dist(name, python3_pkgversion=self.python3_pkgversion)
4569036
                self.output_lines.append(dep)
a613e17
            elif len(together) == 1:
4569036
                self.output_lines.append(together[0])
a613e17
            else:
4569036
                self.output_lines.append(f"({' with '.join(together)})")
50645e1
50645e1
    def check(self, *, source=None):
50645e1
        """End current pass if any unsatisfied dependencies were output"""
50645e1
        if self.missing_requirements:
50645e1
            print_err(f'Exiting dependency generation pass: {source}')
50645e1
            raise EndPass(source)
50645e1
f8a3343
    def extend(self, requirement_strs, **kwargs):
50645e1
        """add() several requirements"""
50645e1
        for req_str in requirement_strs:
f8a3343
            self.add(req_str, **kwargs)
fdf5116
bd78901
    def readd_ignored_alien_requirements(self, **kwargs):
bd78901
        """add() previously ignored alien requirements again."""
bd78901
        requirements, self.ignored_alien_requirements = self.ignored_alien_requirements, []
bd78901
        kwargs.setdefault('source', 'Previously ignored alien requirements')
bd78901
        self.extend(requirements, **kwargs)
bd78901
50464a4
07577de
def toml_load(opened_binary_file):
50645e1
    try:
07577de
        # tomllib is in the standard library since 3.11.0b1
5ab7319
        import tomllib
07577de
    except ImportError:
06b21e1
        try:
5ab7319
            import tomli as tomllib
06b21e1
        except ImportError as e:
06b21e1
            print_err('Import error:', e)
06b21e1
            # already echoed by the %pyproject_buildrequires macro
06b21e1
            sys.exit(0)
5ab7319
    return tomllib.load(opened_binary_file)
07577de
07577de
07577de
def get_backend(requirements):
07577de
    try:
07577de
        f = open('pyproject.toml', 'rb')
07577de
    except FileNotFoundError:
07577de
        pyproject_data = {}
07577de
    else:
50645e1
        with f:
07577de
            pyproject_data = toml_load(f)
50645e1
50464a4
    buildsystem_data = pyproject_data.get('build-system', {})
50645e1
    requirements.extend(
50464a4
        buildsystem_data.get('requires', ()),
50645e1
        source='build-system.requires',
50645e1
    )
50645e1
50645e1
    backend_name = buildsystem_data.get('build-backend')
50645e1
    if not backend_name:
6210f94
        # https://www.python.org/dev/peps/pep-0517/:
6210f94
        # If the pyproject.toml file is absent, or the build-backend key is
6210f94
        # missing, the source tree is not using this specification, and tools
6210f94
        # should revert to the legacy behaviour of running setup.py
6210f94
        # (either directly, or by implicitly invoking the [following] backend).
5b1caad
        # If setup.py is also not present program will mimick pip's behavior
5b1caad
        # and end with an error.
5b1caad
        if not os.path.exists('setup.py'):
5b1caad
            raise FileNotFoundError('File "setup.py" not found for legacy project.')
6210f94
        backend_name = 'setuptools.build_meta:__legacy__'
6210f94
ff39661
        # Note: For projects without pyproject.toml, this was already echoed
ff39661
        # by the %pyproject_buildrequires macro, but this also handles cases
ff39661
        # with pyproject.toml without a specified build backend.
ff39661
        # If the default requirements change, also change them in the macro!
50464a4
        requirements.add('setuptools >= 40.8', source='default build backend')
50464a4
        requirements.add('wheel', source='default build backend')
50645e1
50645e1
    requirements.check(source='build backend')
50645e1
50645e1
    backend_path = buildsystem_data.get('backend-path')
50645e1
    if backend_path:
290941c
        # PEP 517 example shows the path as a list, but some projects don't follow that
290941c
        if isinstance(backend_path, str):
290941c
            backend_path = [backend_path]
290941c
        sys.path = backend_path + sys.path
50645e1
6210f94
    module_name, _, object_name = backend_name.partition(":")
6210f94
    backend_module = importlib.import_module(module_name)
6210f94
6210f94
    if object_name:
6210f94
        return getattr(backend_module, object_name)
6210f94
6210f94
    return backend_module
fdf5116
fdf5116
50645e1
def generate_build_requirements(backend, requirements):
50464a4
    get_requires = getattr(backend, 'get_requires_for_build_wheel', None)
50645e1
    if get_requires:
156e2fc
        new_reqs = get_requires(config_settings=requirements.config_settings)
50645e1
        requirements.extend(new_reqs, source='get_requires_for_build_wheel')
3b95c7d
        requirements.check(source='get_requires_for_build_wheel')
fdf5116
fdf5116
bd78901
def parse_metadata_file(metadata_file):
bd78901
    return email.parser.Parser().parse(metadata_file, headersonly=True)
bd78901
bd78901
bd78901
def requires_from_parsed_metadata_file(message):
40f6765
    return {k: message.get_all(k, ()) for k in ('Requires', 'Requires-Dist')}
40f6765
40f6765
bd78901
def package_name_from_parsed_metadata_file(message):
bd78901
    return message.get('name')
bd78901
bd78901
bd78901
def package_name_and_requires_from_metadata_file(metadata_file):
bd78901
    message = parse_metadata_file(metadata_file)
bd78901
    package_name = package_name_from_parsed_metadata_file(message)
bd78901
    requires = requires_from_parsed_metadata_file(message)
bd78901
    return package_name, requires
bd78901
bd78901
40f6765
def generate_run_requirements_hook(backend, requirements):
50464a4
    hook_name = 'prepare_metadata_for_build_wheel'
50464a4
    prepare_metadata = getattr(backend, hook_name, None)
bc156c4
    if not prepare_metadata:
bc156c4
        raise ValueError(
40f6765
            'The build backend cannot provide build metadata '
40f6765
            '(incl. runtime requirements) before build. '
40f6765
            'Use the provisional -w flag to build the wheel and parse the metadata from it, '
40f6765
            'or use the -R flag not to generate runtime dependencies.'
bc156c4
        )
156e2fc
    dir_basename = prepare_metadata('.', config_settings=requirements.config_settings)
40f6765
    with open(dir_basename + '/METADATA') as metadata_file:
bd78901
        name, requires = package_name_and_requires_from_metadata_file(metadata_file)
bd78901
        for key, req in requires.items():
bd78901
            requirements.extend(req,
bd78901
                                package_name=name,
bd78901
                                source=f'hook generated metadata: {key} ({name})')
40f6765
40f6765
40f6765
def find_built_wheel(wheeldir):
40f6765
    wheels = glob.glob(os.path.join(wheeldir, '*.whl'))
40f6765
    if not wheels:
40f6765
        return None
40f6765
    if len(wheels) > 1:
40f6765
        raise RuntimeError('Found multiple wheels in %{_pyproject_wheeldir}, '
40f6765
                           'this is not supported with %pyproject_buildrequires -w.')
40f6765
    return wheels[0]
40f6765
40f6765
40f6765
def generate_run_requirements_wheel(backend, requirements, wheeldir):
40f6765
    # Reuse the wheel from the previous round of %pyproject_buildrequires (if it exists)
40f6765
    wheel = find_built_wheel(wheeldir)
40f6765
    if not wheel:
40f6765
        import pyproject_wheel
156e2fc
        returncode = pyproject_wheel.build_wheel(
156e2fc
            wheeldir=wheeldir,
156e2fc
            stdout=sys.stderr,
156e2fc
            config_settings=requirements.config_settings,
156e2fc
        )
40f6765
        if returncode != 0:
40f6765
            raise RuntimeError('Failed to build the wheel for %pyproject_buildrequires -w.')
40f6765
        wheel = find_built_wheel(wheeldir)
40f6765
    if not wheel:
40f6765
        raise RuntimeError('Cannot locate the built wheel for %pyproject_buildrequires -w.')
40f6765
40f6765
    print_err(f'Reading metadata from {wheel}')
40f6765
    with zipfile.ZipFile(wheel) as wheelfile:
40f6765
        for name in wheelfile.namelist():
40f6765
            if name.count('/') == 1 and name.endswith('.dist-info/METADATA'):
40f6765
                with io.TextIOWrapper(wheelfile.open(name), encoding='utf-8') as metadata_file:
bd78901
                    name, requires = package_name_and_requires_from_metadata_file(metadata_file)
bd78901
                    for key, req in requires.items():
bd78901
                        requirements.extend(req,
bd78901
                                            package_name=name,
bd78901
                                            source=f'built wheel metadata: {key} ({name})')
40f6765
                break
40f6765
        else:
40f6765
            raise RuntimeError('Could not find *.dist-info/METADATA in built wheel.')
40f6765
40f6765
40f6765
def generate_run_requirements(backend, requirements, *, build_wheel, wheeldir):
40f6765
    if build_wheel:
40f6765
        generate_run_requirements_wheel(backend, requirements, wheeldir)
40f6765
    else:
40f6765
        generate_run_requirements_hook(backend, requirements)
bc156c4
bc156c4
8a60635
def generate_tox_requirements(toxenv, requirements):
38ef5fb
    toxenv = ','.join(toxenv)
7e1a8fd
    requirements.add('tox-current-env >= 0.0.6', source='tox itself')
fda0a23
    requirements.check(source='tox itself')
7e1a8fd
    with tempfile.NamedTemporaryFile('r') as deps, \
7e1a8fd
        tempfile.NamedTemporaryFile('r') as extras, \
7e1a8fd
            tempfile.NamedTemporaryFile('r') as provision:
fda0a23
        r = subprocess.run(
9f3eea2
            [sys.executable, '-m', 'tox',
9f3eea2
             '--print-deps-to', deps.name,
9f3eea2
             '--print-extras-to', extras.name,
7e1a8fd
             '--no-provision', provision.name,
3deb3f4
             '-q', '-r', '-e', toxenv],
d5c3fb3
            check=False,
fda0a23
            encoding='utf-8',
fda0a23
            stdout=subprocess.PIPE,
fda0a23
            stderr=subprocess.STDOUT,
fda0a23
        )
fda0a23
        if r.stdout:
d5c3fb3
            print_err(r.stdout, end='')
7e1a8fd
7e1a8fd
        provision_content = provision.read()
7e1a8fd
        if provision_content and r.returncode != 0:
7e1a8fd
            provision_requires = json.loads(provision_content)
7e1a8fd
            if 'minversion' in provision_requires:
7e1a8fd
                requirements.add(f'tox >= {provision_requires["minversion"]}',
7e1a8fd
                                 source='tox provision (minversion)')
7e1a8fd
            if 'requires' in provision_requires:
7e1a8fd
                requirements.extend(provision_requires["requires"],
7e1a8fd
                                    source='tox provision (requires)')
7e1a8fd
            requirements.check(source='tox provision')  # this terminates the script
7e1a8fd
            raise RuntimeError(
7e1a8fd
                'Dependencies requested by tox provisioning appear installed, '
7e1a8fd
                'but tox disagreed.')
7e1a8fd
        else:
7e1a8fd
            r.check_returncode()
99d952c
9f3eea2
        deplines = deps.read().splitlines()
aeb21f6
        packages = convert_requirements_txt(deplines)
9f3eea2
        requirements.add_extras(*extras.read().splitlines())
99d952c
        requirements.extend(packages,
7ba1a33
                            source=f'tox --print-deps-only: {toxenv}')
8a60635
8a60635
5561755
def python3dist(name, op=None, version=None, python3_pkgversion="3"):
5561755
    prefix = f"python{python3_pkgversion}dist"
5561755
50645e1
    if op is None:
50645e1
        if version is not None:
50645e1
            raise AssertionError('op and version go together')
5561755
        return f'{prefix}({name})'
50645e1
    else:
5561755
        return f'{prefix}({name}) {op} {version}'
50645e1
50645e1
d6e6bb7
def generate_requires(
40f6765
    *, include_runtime=False, build_wheel=False, wheeldir=None, toxenv=None, extras=None,
390b971
    get_installed_version=importlib.metadata.version,  # for dep injection
4569036
    generate_extras=False, python3_pkgversion="3", requirement_files=None, use_build_system=True,
156e2fc
    output, config_settings=None,
d6e6bb7
):
d262d90
    """Generate the BuildRequires for the project in the current directory
d262d90
4569036
    The generated BuildRequires are written to the provided output.
4569036
d262d90
    This is the main Python entry point.
d262d90
    """
5561755
    requirements = Requirements(
2ecbed7
        get_installed_version, extras=extras or [],
a613e17
        generate_extras=generate_extras,
156e2fc
        python3_pkgversion=python3_pkgversion,
156e2fc
        config_settings=config_settings,
5561755
    )
50645e1
fdf5116
    try:
d6ad9a7
        if (include_runtime or toxenv) and not use_build_system:
d6ad9a7
            raise ValueError('-N option cannot be used in combination with -r, -e, -t, -x options')
d6ad9a7
        if requirement_files:
d6ad9a7
            for req_file in requirement_files:
aeb21f6
                requirements.extend(
aeb21f6
                    convert_requirements_txt(req_file, pathlib.Path(req_file.name)),
6ee7a6b
                    source=f'requirements file {req_file.name}'
aeb21f6
                )
f8e0c24
            requirements.check(source='all requirements files')
d6ad9a7
        if use_build_system:
d6ad9a7
            backend = get_backend(requirements)
d6ad9a7
            generate_build_requirements(backend, requirements)
38ef5fb
        if toxenv:
8a60635
            include_runtime = True
8a60635
            generate_tox_requirements(toxenv, requirements)
bc156c4
        if include_runtime:
40f6765
            generate_run_requirements(backend, requirements, build_wheel=build_wheel, wheeldir=wheeldir)
50645e1
    except EndPass:
5abd40f
        return
4569036
    finally:
4569036
        output.write_text(os.linesep.join(requirements.output_lines) + os.linesep)
50645e1
50645e1
50645e1
def main(argv):
50645e1
    parser = argparse.ArgumentParser(
222ec5f
        description='Generate BuildRequires for a Python project.',
222ec5f
        prog='%pyproject_buildrequires',
222ec5f
        add_help=False,
222ec5f
    )
222ec5f
    parser.add_argument(
222ec5f
        '--help', action='help',
222ec5f
        default=argparse.SUPPRESS,
222ec5f
        help=argparse.SUPPRESS,
50645e1
    )
50645e1
    parser.add_argument(
8c8afba
        '-r', '--runtime', action='store_true', default=True,
222ec5f
        help=argparse.SUPPRESS,  # Generate run-time requirements (backwards-compatibility only)
8c8afba
    )
8c8afba
    parser.add_argument(
8b3af75
        '--generate-extras', action='store_true',
8b3af75
        help=argparse.SUPPRESS,
40f6765
    )
40f6765
    parser.add_argument(
8b3af75
        '-p', '--python3_pkgversion', metavar='PYTHON3_PKGVERSION',
8b3af75
        default="3", help=argparse.SUPPRESS,
40f6765
    )
40f6765
    parser.add_argument(
4569036
        '--output', type=pathlib.Path, required=True, help=argparse.SUPPRESS,
4569036
    )
4569036
    parser.add_argument(
8b3af75
        '--wheeldir', metavar='PATH', default=None,
8b3af75
        help=argparse.SUPPRESS,
50645e1
    )
50645e1
    parser.add_argument(
8b3af75
        '-x', '--extras', metavar='EXTRAS', action='append',
8b3af75
        help='comma separated list of "extras" for runtime requirements '
8b3af75
             '(e.g. -x testing,feature-x) (implies --runtime, can be repeated)',
fda0a23
    )
fda0a23
    parser.add_argument(
fda0a23
        '-t', '--tox', action='store_true',
8a60635
        help=('generate test tequirements from tox environment '
8a60635
              '(implies --runtime)'),
50645e1
    )
d6e6bb7
    parser.add_argument(
8b3af75
        '-e', '--toxenv', metavar='TOXENVS', action='append',
8b3af75
        help=('specify tox environments (comma separated and/or repeated)'
8b3af75
              '(implies --tox)'),
d6e6bb7
    )
5561755
    parser.add_argument(
8b3af75
        '-w', '--wheel', action='store_true', default=False,
8b3af75
        help=('Generate run-time requirements by building the wheel '
8b3af75
              '(useful for build backends without the prepare_metadata_for_build_wheel hook)'),
a613e17
    )
a613e17
    parser.add_argument(
8b3af75
        '-R', '--no-runtime', action='store_false', dest='runtime',
8b3af75
        help="Don't generate run-time requirements (implied by -N)",
5561755
    )
d6ad9a7
    parser.add_argument(
d6ad9a7
        '-N', '--no-use-build-system', dest='use_build_system',
d6ad9a7
        action='store_false', help='Use -N to indicate that project does not use any build system',
d6ad9a7
    )
d6ad9a7
    parser.add_argument(
222ec5f
        'requirement_files', nargs='*', type=argparse.FileType('r'),
222ec5f
        metavar='REQUIREMENTS.TXT',
d6ad9a7
        help=('Add buildrequires from file'),
d6ad9a7
    )
156e2fc
    parser.add_argument(
156e2fc
        '-C',
156e2fc
        dest='config_settings',
156e2fc
        action='append',
156e2fc
        help='Configuration settings to pass to the PEP 517 backend',
156e2fc
    )
50645e1
50645e1
    args = parser.parse_args(argv)
fda0a23
8c8afba
    if not args.use_build_system:
8c8afba
        args.runtime = False
8c8afba
40f6765
    if args.wheel:
40f6765
        if not args.wheeldir:
40f6765
            raise ValueError('--wheeldir must be set when -w.')
40f6765
c975fbe
    if args.toxenv:
fda0a23
        args.tox = True
fda0a23
fda0a23
    if args.tox:
c975fbe
        args.runtime = True
38ef5fb
        if not args.toxenv:
38ef5fb
            _default = f'py{sys.version_info.major}{sys.version_info.minor}'
38ef5fb
            args.toxenv = [os.getenv('RPM_TOXENV', _default)]
fda0a23
262f6d3
    if args.extras:
262f6d3
        args.runtime = True
50645e1
50645e1
    try:
d6e6bb7
        generate_requires(
d6e6bb7
            include_runtime=args.runtime,
40f6765
            build_wheel=args.wheel,
40f6765
            wheeldir=args.wheeldir,
8a60635
            toxenv=args.toxenv,
d6e6bb7
            extras=args.extras,
a613e17
            generate_extras=args.generate_extras,
5561755
            python3_pkgversion=args.python3_pkgversion,
d6ad9a7
            requirement_files=args.requirement_files,
d6ad9a7
            use_build_system=args.use_build_system,
4569036
            output=args.output,
156e2fc
            config_settings=parse_config_settings_args(args.config_settings),
d6e6bb7
        )
fda0a23
    except Exception:
50645e1
        # Log the traceback explicitly (it's useful debug info)
50645e1
        traceback.print_exc()
50645e1
        exit(1)
fdf5116
fdf5116
50645e1
if __name__ == '__main__':
50645e1
    main(sys.argv[1:])