diff --git a/python-rpm-generators.spec b/python-rpm-generators.spec index bb2505e94f19dea1b5ee9a61b84179797f31e525..ca3bffc1a7f48c0f710edaadb8a6cbf0ccd2298d 100644 --- a/python-rpm-generators.spec +++ b/python-rpm-generators.spec @@ -1,22 +1,17 @@ %undefine py_auto_byte_compile Name: python-rpm-generators -Version: 14 -Release: 1 +Version: 9 +Release: 5 Summary: Dependency generators for Python RPMs -License: GPL-2.0-or-later AND LGPL-2.1-or-later AND (Public Domain OR LGPL-2.1-or-later OR GPL-2.0-or-later) +License: GPLv2+ URL: https://src.fedoraproject.org/rpms/python-rpm-generators Source0: https://raw.githubusercontent.com/rpm-software-management/rpm/102eab50b3d0d6546dfe082eac0ade21e6b3dbf1/COPYING -Source1: python.attr -Source2: pythondist.attr -# This was crafted in-place as a fork of python.attr, hence also GPL-2.0-or-later -Source3: pythonname.attr -# This one is also originally from RPM, but it has its own license declaration: LGPL-2.1-or-later -Source4: pythondistdeps.py -# This was crafted in-place with the following license declaration: -# Public-Domain OR CC0-1.0 OR LGPL-2.1-or-later OR GPL-2.0-or-later -Source5: pythonbundles.py +Source1: python.attr +Source2: pythondist.attr +Source3: pythondeps.sh +Source4: pythondistdeps.py BuildArch: noarch @@ -26,7 +21,6 @@ Fedora's dependency generators for Python RPMS. %package -n python3-rpm-generators Summary: %{summary} Requires: python3-setuptools -Requires: rpm > 4.15.90 Conflicts: rpm-build < 4.13.0.1-2 Conflicts: python-rpm-macros < 3-35 @@ -38,21 +32,18 @@ Fedora's dependency generators for Python RPMS. cp -a %{sources} . %install -install -Dpm0644 -t %{buildroot}%{_fileattrsdir} *.attr -install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} *.py +install -Dpm0644 -t %{buildroot}%{_fileattrsdir} python.attr pythondist.attr +install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} pythondeps.sh pythondistdeps.py %files -n python3-rpm-generators +%defattr(-,root,root) %license COPYING %{_fileattrsdir}/python.attr %{_fileattrsdir}/pythondist.attr -%{_fileattrsdir}/pythonname.attr +%{_rpmconfigdir}/pythondeps.sh %{_rpmconfigdir}/pythondistdeps.py -%{_rpmconfigdir}/pythonbundles.py %changelog -* Sat Oct 19 2024 Funda Wang - 14-1 -- update to latest upstream version - * Tue Aug 06 2024 chenhuihan - 9-5 - Fix python provide and requires diff --git a/python.attr b/python.attr index cf5ae390d62c308d9d3ce153aceeb75421544955..f5d2dff7347d9e820a6879a00d2546ddeeced055 100644 --- a/python.attr +++ b/python.attr @@ -1,31 +1,4 @@ -%__python_provides() %{lua: - -- Match buildroot/payload paths of the form - -- /PATH/OF/BUILDROOT/usr/bin/pythonMAJOR.MINOR - -- generating a line of the form - -- python(abi) = MAJOR.MINOR - -- (Don't match against -config tools e.g. /usr/bin/python2.6-config) - local path = rpm.expand('%1') - -- Use /usr prefix by default, and /app for flatpak builds - local prefix = rpm.expand('%{?!flatpak:/usr}%{?flatpak:/app}') - if path:match(prefix .. '/bin/python%d+%.%d+$') then - local provides = path:gsub('.*' .. prefix .. '/bin/python(%d+%.%d+)', 'python(abi) = %1') - print(provides) - end -} - -%__python_requires() %{lua: - -- Match buildroot paths of the form - -- /PATH/OF/BUILDROOT/usr/lib/pythonMAJOR.MINOR/ and - -- /PATH/OF/BUILDROOT/usr/lib64/pythonMAJOR.MINOR/ - -- generating a line of the form: - -- python(abi) = MAJOR.MINOR - local path = rpm.expand('%1') - -- Use /usr prefix by default, and /app for flatpak builds - local prefix = rpm.expand('%{?!flatpak:/usr}%{?flatpak:/app}') - if path:match(prefix .. '/lib%d*/python%d+%.%d+/.*') then - local requires = path:gsub('.*' .. prefix .. '/lib%d*/python(%d+%.%d+)/.*', 'python(abi) = %1') - print(requires) - end -} - -%__python_path ^((%{?!flatpak:/usr}%{?flatpak:/app}/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/.*\\.(py[oc]?|so))|(%{_bindir}/python[[:digit:]]+\\.[[:digit:]]+))$ +%__python_provides %{_rpmconfigdir}/pythondeps.sh --provides +%__python_requires %{_rpmconfigdir}/pythondeps.sh --requires +%__python_path ^((/usr/lib(64)?/python[[:digit:]]\\.[[:digit:]]+/.*\\.(py[oc]?|so))|(^%{_bindir}/python[[:digit:]]\\.[[:digit:]]+))$ +%__python_magic [Pp]ython.*(executable|byte-compiled) diff --git a/pythonbundles.py b/pythonbundles.py deleted file mode 100644 index b0e5ecf6b53150a005ad603cf438846e021e3da3..0000000000000000000000000000000000000000 --- a/pythonbundles.py +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/python3 -sB -# (imports pythondistdeps from /usr/lib/rpm, hence -B) -# -# This program is free software. -# -# It is placed in the public domain or under the CC0-1.0-Universal license, -# whichever you choose. -# -# Alternatively, it may be redistributed and/or modified under the terms of -# the LGPL version 2.1 (or later) or GPL version 2 (or later). -# -# Use this script to generate bundled provides, e.g.: -# ./pythonbundles.py setuptools-47.1.1/pkg_resources/_vendor/vendored.txt - -import pathlib -import sys - -from packaging import requirements - -import pythondistdeps - -def generate_bundled_provides(paths, namespace): - provides = set() - - for path in paths: - for line in path.read_text().splitlines(): - line, _, comment = line.partition('#') - if comment.startswith('egg='): - # not a real comment - # e.g. git+https://github.com/monty/spam.git@master#egg=spam&... - egg, *_ = comment.strip().partition(' ') - egg, *_ = egg.strip().partition('&') - name = pythondistdeps.normalize_name(egg[4:]) - provides.add(f'Provides: bundled({namespace}({name}))') - continue - line = line.strip() - if line: - requirement = requirements.Requirement(line) - for spec in requirement.specifier: - if spec.operator == '==': - version = spec.version - break - else: - raise ValueError('pythonbundles.py only handles exactly one == requirement') - name = pythondistdeps.normalize_name(requirement.name) - bundled_name = f"bundled({namespace}({name}))" - python_provide = pythondistdeps.convert(bundled_name, '==', version) - provides.add(f'Provides: {python_provide}') - - return provides - - -def compare(expected, given): - stripped = (l.strip() for l in given) - no_comments = set(l for l in stripped if not l.startswith('#')) - no_comments.discard('') - if expected == no_comments: - return True - extra_expected = expected - no_comments - extra_given = no_comments - expected - if extra_expected: - print('Missing expected provides:', file=sys.stderr) - for provide in sorted(extra_expected): - print(f' - {provide}', file=sys.stderr) - if extra_given: - print('Redundant unexpected provides:', file=sys.stderr) - for provide in sorted(extra_given): - print(f' + {provide}', file=sys.stderr) - return False - - -if __name__ == '__main__': - import argparse - - parser = argparse.ArgumentParser(prog=sys.argv[0], - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument('vendored', metavar='VENDORED.TXT', nargs='+', type=pathlib.Path, - help='Upstream information about vendored libraries') - parser.add_argument('-c', '--compare-with', action='store', - help='A string value to compare with and verify') - parser.add_argument('-n', '--namespace', action='store', - help='What namespace of provides will used', default='python3dist') - args = parser.parse_args() - - provides = generate_bundled_provides(args.vendored, args.namespace) - - if args.compare_with: - given = args.compare_with.splitlines() - same = compare(provides, given) - if not same: - sys.exit(1) - else: - for provide in sorted(provides): - print(provide) diff --git a/pythondist.attr b/pythondist.attr index ede3a51511518f1a7320c3ef0c023f09a963cf05..2bf737ae33876b23006fc0965b0b7a4e78032ff4 100644 --- a/pythondist.attr +++ b/pythondist.attr @@ -1,3 +1,3 @@ -%__pythondist_provides %{_rpmconfigdir}/pythondistdeps.py --provides --normalized-names-format pep503 --package-name %{name} --majorver-provides-versions %{__default_python3_version} %{?!_python_dist_allow_version_zero:--fail-if-zero} -%__pythondist_requires %{_rpmconfigdir}/pythondistdeps.py --requires --normalized-names-format pep503 --package-name %{name} %{?!_python_no_extras_requires:--require-extras-subpackages} --console-scripts-nodep-setuptools-since 3.10 -%__pythondist_path ^%{?!flatpak:/usr}%{?flatpak:/app}/lib(64)?/python[3-9]\\.[[:digit:]]+/site-packages/[^/]+\\.(dist-info|egg-info|egg-link)$ +%__pythondist_provides %{_rpmconfigdir}/pythondistdeps.py --provides --majorver-provides +%__pythondist_requires %{_rpmconfigdir}/pythondistdeps.py --requires +%__pythondist_path ^/usr/lib(64)?/python[[:digit:]]\\.[[:digit:]]+/site-packages/[^/]+\\.(dist-info|egg-info|egg-link)$ diff --git a/pythondistdeps.py b/pythondistdeps.py index b43ed398dd6b4141af9a80a04c6930ad6f9d43d0..eb91ac9f8f39779d1ad1f66e2779bd4a4413db53 100644 --- a/pythondistdeps.py +++ b/pythondistdeps.py @@ -1,9 +1,8 @@ -#!/usr/bin/python3 -s +#!/usr/bin/python3 # -*- coding: utf-8 -*- # # Copyright 2010 Per Øyvind Karlsen # Copyright 2015 Neal Gompa -# Copyright 2020 SUSE LLC # # This program is free software. It may be redistributed and/or modified under # the terms of the LGPL version 2.1 (or later). @@ -12,615 +11,248 @@ # from __future__ import print_function -import argparse -from os.path import dirname, sep -import re -from sys import argv, stdin, stderr, version_info -from sysconfig import get_path +from getopt import getopt +from os.path import basename, dirname, isdir, sep +from sys import argv, stdin, version +from distutils.sysconfig import get_python_lib from warnings import warn -from packaging.requirements import Requirement as Requirement_ -from packaging.version import parse -import packaging.markers -# Monkey patching packaging.markers to handle extras names in a -# case-insensitive manner: -# pip considers dnspython[DNSSEC] and dnspython[dnssec] to be equal, but -# packaging markers treat extras in a case-sensitive manner. To solve this -# issue, we introduce a comparison operator that compares case-insensitively -# if both sides of the comparison are strings. And then we inject this -# operator into packaging.markers to be used when comparing names of extras. -# Fedora BZ: https://bugzilla.redhat.com/show_bug.cgi?id=1936875 -# Upstream issue: https://discuss.python.org/t/what-extras-names-are-treated-as-equal-and-why/7614 -# - After it's established upstream what is the canonical form of an extras -# name, we plan to open an issue with packaging to hopefully solve this -# there without having to resort to monkeypatching. -def str_lower_eq(a, b): - if isinstance(a, str) and isinstance(b, str): - return a.lower() == b.lower() - else: - return a == b -packaging.markers._operators["=="] = str_lower_eq - -try: - from importlib.metadata import PathDistribution -except ImportError: - from importlib_metadata import PathDistribution - -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path - - -def normalize_name(name): - """https://www.python.org/dev/peps/pep-0503/#normalized-names""" - return re.sub(r'[-_.]+', '-', name).lower() - - -def legacy_normalize_name(name): - """Like pkg_resources Distribution.key property""" - return re.sub(r'[-_]+', '-', name).lower() - - -class Requirement(Requirement_): - def __init__(self, requirement_string): - super(Requirement, self).__init__(requirement_string) - self.normalized_name = normalize_name(self.name) - self.legacy_normalized_name = legacy_normalize_name(self.name) - - -class Distribution(PathDistribution): - def __init__(self, path): - super(Distribution, self).__init__(Path(path)) - - # Check that the initialization went well and metadata are not missing or corrupted - # name is the most important attribute, if it doesn't exist, import failed - if not self.name or not isinstance(self.name, str): - print("*** PYTHON_METADATA_FAILED_TO_PARSE_ERROR___SEE_STDERR ***") - print('Error: Python metadata at `{}` are missing or corrupted.'.format(path), file=stderr) - exit(65) # os.EX_DATAERR - - self.normalized_name = normalize_name(self.name) - self.legacy_normalized_name = legacy_normalize_name(self.name) - self.requirements = [Requirement(r) for r in self.requires or []] - self.extras = [ - v.lower() for k, v in self.metadata.items() if k == 'Provides-Extra'] - self.py_version = self._parse_py_version(path) - - # `name` is defined as a property exactly like this in Python 3.10 in the - # PathDistribution class. Due to that we can't redefine `name` as a normal - # attribute. So we copied the Python 3.10 definition here into the code so - # that it works also on previous Python/importlib_metadata versions. - @property - def name(self): - """Return the 'Name' metadata for the distribution package or None.""" - return self.metadata.get('Name') - - def _parse_py_version(self, path): - # Try to parse the Python version from the path the metadata - # resides at (e.g. /usr/lib/pythonX.Y/site-packages/...) - res = re.search(r"/python(?P\d+\.\d+)/", path) - if res: - return res.group('pyver') - # If that hasn't worked, attempt to parse it from the metadata - # directory name - res = re.search(r"-py(?P\d+.\d+)[.-]egg-info$", path) - if res: - return res.group('pyver') - return None - - def requirements_for_extra(self, extra): - extra_deps = [] - # we are only interested in dependencies with extra == 'our_extra' marker - for req in self.requirements: - # no marker at all, nothing to evaluate - if not req.marker: - continue - # does the marker include extra == 'our_extra'? - # we can only evaluate the marker as a whole, - # so we evaluate it twice (using 2 different marker_envs) - # and see if it only evaluates to True with our extra - if (req.marker.evaluate(get_marker_env(self, extra)) and - not req.marker.evaluate(get_marker_env(self, None))): - extra_deps.append(req) - return extra_deps - - def __repr__(self): - return '{} from {}'.format(self.name, self._path) - - -class RpmVersion(): - def __init__(self, version_id): - version = parse(version_id) - if isinstance(version._version, str): - self.version = version._version - else: - self.epoch = version._version.epoch - self.version = list(version._version.release) - self.pre = version._version.pre - self.dev = version._version.dev - self.post = version._version.post - # version.local is ignored as it is not expected to appear - # in public releases - # https://www.python.org/dev/peps/pep-0440/#local-version-identifiers - - def is_legacy(self): - return isinstance(self.version, str) - - def increment(self): - self.version[-1] += 1 - self.pre = None - self.dev = None - self.post = None - return self - - def is_zero(self): - return self.__str__() == '0' - - def __str__(self): - if self.is_legacy(): - return self.version - if self.epoch: - rpm_epoch = str(self.epoch) + ':' +opts, args = getopt( + argv[1:], 'hPRrCEMmLl:', + ['help', 'provides', 'requires', 'recommends', 'conflicts', 'extras', 'majorver-provides', 'majorver-only', 'legacy-provides' , 'legacy']) + +Provides = False +Requires = False +Recommends = False +Conflicts = False +Extras = False +Provides_PyMajorVer_Variant = False +PyMajorVer_Deps = False +legacy_Provides = False +legacy = False + +for o, a in opts: + if o in ('-h', '--help'): + print('-h, --help\tPrint help') + print('-P, --provides\tPrint Provides') + print('-R, --requires\tPrint Requires') + print('-r, --recommends\tPrint Recommends') + print('-C, --conflicts\tPrint Conflicts') + print('-E, --extras\tPrint Extras ') + print('-M, --majorver-provides\tPrint extra Provides with Python major version only') + print('-m, --majorver-only\tPrint Provides/Requires with Python major version only') + print('-L, --legacy-provides\tPrint extra legacy pythonegg Provides') + print('-l, --legacy\tPrint legacy pythonegg Provides/Requires instead') + exit(1) + elif o in ('-P', '--provides'): + Provides = True + elif o in ('-R', '--requires'): + Requires = True + elif o in ('-r', '--recommends'): + Recommends = True + elif o in ('-C', '--conflicts'): + Conflicts = True + elif o in ('-E', '--extras'): + Extras = True + elif o in ('-M', '--majorver-provides'): + Provides_PyMajorVer_Variant = True + elif o in ('-m', '--majorver-only'): + PyMajorVer_Deps = True + elif o in ('-L', '--legacy-provides'): + legacy_Provides = True + elif o in ('-l', '--legacy'): + legacy = True + +if Requires: + py_abi = True +else: + py_abi = False +py_deps = {} +if args: + files = args +else: + files = stdin.readlines() + +for f in files: + f = f.strip() + lower = f.lower() + name = 'python(abi)' + # add dependency based on path, versioned if within versioned python directory + if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')): + if name not in py_deps: + py_deps[name] = [] + purelib = get_python_lib(standard_lib=0, plat_specific=0).split(version[:3])[0] + platlib = get_python_lib(standard_lib=0, plat_specific=1).split(version[:3])[0] + for lib in (purelib, platlib): + if lib in f: + spec = ('==', f.split(lib)[1].split(sep)[0]) + if spec not in py_deps[name]: + py_deps[name].append(spec) + + # XXX: hack to workaround RPM internal dependency generator not passing directories + lower_dir = dirname(lower) + if lower_dir.endswith('.egg') or \ + lower_dir.endswith('.egg-info') or \ + lower_dir.endswith('.dist-info'): + lower = lower_dir + f = dirname(f) + # Determine provide, requires, conflicts & recommends based on egg/dist metadata + if lower.endswith('.egg') or \ + lower.endswith('.egg-info') or \ + lower.endswith('.dist-info'): + # This import is very slow, so only do it if needed + from pkg_resources import Distribution, FileMetadata, PathMetadata, Requirement + dist_name = basename(f) + if isdir(f): + path_item = dirname(f) + metadata = PathMetadata(path_item, f) else: - rpm_epoch = '' - while len(self.version) > 1 and self.version[-1] == 0: - self.version.pop() - rpm_version = '.'.join(str(x) for x in self.version) - if self.pre: - rpm_suffix = '~{}'.format(''.join(str(x) for x in self.pre)) - elif self.dev: - rpm_suffix = '~~{}'.format(''.join(str(x) for x in self.dev)) - elif self.post: - rpm_suffix = '^post{}'.format(self.post[1]) - else: - rpm_suffix = '' - return '{}{}{}'.format(rpm_epoch, rpm_version, rpm_suffix) - - -def convert_compatible(name, operator, version_id): - if version_id.endswith('.*'): - print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***") - print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr) - exit(65) # os.EX_DATAERR - version = RpmVersion(version_id) - if version.is_legacy(): - # LegacyVersions are not supported in this context - print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***") - print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr) - exit(65) # os.EX_DATAERR - if len(version.version) == 1: - print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***") - print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr) - exit(65) # os.EX_DATAERR - upper_version = RpmVersion(version_id) - upper_version.version.pop() - upper_version.increment() - return '({} >= {} with {} < {})'.format( - name, version, name, upper_version) - - -def convert_equal(name, operator, version_id): - if version_id.endswith('.*'): - version_id = version_id[:-2] + '.0' - return convert_compatible(name, '~=', version_id) - version = RpmVersion(version_id) - return '{} = {}'.format(name, version) - - -def convert_arbitrary_equal(name, operator, version_id): - if version_id.endswith('.*'): - print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***") - print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr) - exit(65) # os.EX_DATAERR - version = RpmVersion(version_id) - return '{} = {}'.format(name, version) - - -def convert_not_equal(name, operator, version_id): - if version_id.endswith('.*'): - version_id = version_id[:-2] - version = RpmVersion(version_id) - if version.is_legacy(): - # LegacyVersions are not supported in this context - print("*** INVALID_REQUIREMENT_ERROR___SEE_STDERR ***") - print('Invalid requirement: {} {} {}'.format(name, operator, version_id), file=stderr) - exit(65) # os.EX_DATAERR - version_gt = RpmVersion(version_id).increment() - version_gt_operator = '>=' - # Prevent dev and pre-releases from satisfying a < requirement - version = '{}~~'.format(version) - else: - version = RpmVersion(version_id) - version_gt = version - version_gt_operator = '>' - return '({} < {} or {} {} {})'.format( - name, version, name, version_gt_operator, version_gt) - - -def convert_ordered(name, operator, version_id): - if version_id.endswith('.*'): - # PEP 440 does not define semantics for prefix matching - # with ordered comparisons - # see: https://github.com/pypa/packaging/issues/320 - # and: https://github.com/pypa/packaging/issues/321 - # This style of specifier is officially "unsupported", - # even though it is processed. Support may be removed - # in version 21.0. - version_id = version_id[:-2] - version = RpmVersion(version_id) - if operator == '>': - # distutils will allow a prefix match with '>' - operator = '>=' - if operator == '<=': - # distutils will not allow a prefix match with '<=' - operator = '<' - else: - version = RpmVersion(version_id) - # For backwards compatibility, fallback to previous behavior with LegacyVersions - if not version.is_legacy(): - # Prevent dev and pre-releases from satisfying a < requirement - if operator == '<' and not version.pre and not version.dev and not version.post: - version = '{}~~'.format(version) - # Prevent post-releases from satisfying a > requirement - if operator == '>' and not version.pre and not version.dev and not version.post: - version = '{}.0'.format(version) - return '{} {} {}'.format(name, operator, version) - - -OPERATORS = {'~=': convert_compatible, - '==': convert_equal, - '===': convert_arbitrary_equal, - '!=': convert_not_equal, - '<=': convert_ordered, - '<': convert_ordered, - '>=': convert_ordered, - '>': convert_ordered} - - -def convert(name, operator, version_id): - try: - return OPERATORS[operator](name, operator, version_id) - except Exception as exc: - raise RuntimeError("Cannot process Python package version `{}` for name `{}`". - format(version_id, name)) from exc - - -def get_marker_env(dist, extra): - # packaging uses a default environment using - # platform.python_version to evaluate if a dependency is relevant - # based on environment markers [1], - # e.g. requirement `argparse;python_version<"2.7"` - # - # Since we're running this script on one Python version while - # possibly evaluating packages for different versions, we - # set up an environment with the version we want to evaluate. - # - # [1] https://www.python.org/dev/peps/pep-0508/#environment-markers - return {"python_full_version": dist.py_version, - "python_version": dist.py_version, - "extra": extra} - - -def main(): - """To allow this script to be importable (and its classes/functions - reused), actions are defined in the main function and are performed only - when run as a main script.""" - parser = argparse.ArgumentParser(prog=argv[0]) - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument('-P', '--provides', action='store_true', help='Print Provides') - group.add_argument('-R', '--requires', action='store_true', help='Print Requires') - group.add_argument('-r', '--recommends', action='store_true', help='Print Recommends') - group.add_argument('-C', '--conflicts', action='store_true', help='Print Conflicts') - group.add_argument('-E', '--extras', action='store_true', help='[Unused] Generate spec file snippets for extras subpackages') - group_majorver = parser.add_mutually_exclusive_group() - group_majorver.add_argument('-M', '--majorver-provides', action='store_true', help='Print extra Provides with Python major version only') - group_majorver.add_argument('--majorver-provides-versions', action='append', - help='Print extra Provides with Python major version only for listed ' - 'Python VERSIONS (appended or comma separated without spaces, e.g. 2.7,3.9)') - parser.add_argument('-m', '--majorver-only', action='store_true', help='Print Provides/Requires with Python major version only') - parser.add_argument('-n', '--normalized-names-format', action='store', - default="legacy-dots", choices=["pep503", "legacy-dots"], - help='Format of normalized names according to pep503 or legacy format that allows dots [default]') - parser.add_argument('--normalized-names-provide-both', action='store_true', - help='Provide both `pep503` and `legacy-dots` format of normalized names (useful for a transition period)') - parser.add_argument('-L', '--legacy-provides', action='store_true', help='Print extra legacy pythonegg Provides') - parser.add_argument('-l', '--legacy', action='store_true', help='Print legacy pythonegg Provides/Requires instead') - parser.add_argument('--console-scripts-nodep-setuptools-since', action='store', - help='An optional Python version (X.Y), at least 3.8. ' - 'For that version and any newer version, ' - 'a dependency on "setuptools" WILL NOT be generated for packages with console_scripts/gui_scripts entry points. ' - 'By setting this flag, you guarantee that setuptools >= 47.2.0 is used ' - 'during the build of packages for this and any newer Python version.') - parser.add_argument('--require-extras-subpackages', action='store_true', - help="If there is a dependency on a package with extras functionality, require the extras subpackage") - parser.add_argument('--package-name', action='store', help="Name of the RPM package that's being inspected. Required for extras requires/provides to work.") - parser.add_argument('--namespace', action='store', help="Namespace for the printed Requires, Provides, Recommends and Conflicts") - parser.add_argument('--fail-if-zero', action='store_true', help='Fail the script if the automatically generated Provides version was 0, which usually indicates a packaging error.') - parser.add_argument('files', nargs=argparse.REMAINDER, help="Files from the RPM package that are to be inspected, can also be supplied on stdin") - args = parser.parse_args() - - if args.fail_if_zero and not args.provides: - raise parser.error('--fail-if-zero only works with --provides') - - py_abi = args.requires - py_deps = {} - - if args.majorver_provides_versions: - # Go through the arguments (can be specified multiple times), - # and parse individual versions (can be comma-separated) - args.majorver_provides_versions = [v for vstring in args.majorver_provides_versions - for v in vstring.split(",")] - - # If normalized_names_require_pep503 is True we require the pep503 - # normalized name, if it is False we provide the legacy normalized name - normalized_names_require_pep503 = args.normalized_names_format == "pep503" - - # If normalized_names_provide_pep503/legacy is True we provide the - # pep503/legacy normalized name, if it is False we don't - normalized_names_provide_pep503 = \ - args.normalized_names_format == "pep503" or args.normalized_names_provide_both - normalized_names_provide_legacy = \ - args.normalized_names_format == "legacy-dots" or args.normalized_names_provide_both - - # At least one type of normalization must be provided - assert normalized_names_provide_pep503 or normalized_names_provide_legacy - - if args.console_scripts_nodep_setuptools_since: - nodep_setuptools_pyversion = parse(args.console_scripts_nodep_setuptools_since) - if nodep_setuptools_pyversion < parse("3.8"): - print("Only version 3.8+ is supported in --console-scripts-nodep-setuptools-since", file=stderr) - print("*** PYTHON_EXTRAS_ARGUMENT_ERROR___SEE_STDERR ***") - exit(65) # os.EX_DATAERR - else: - nodep_setuptools_pyversion = None - - # Is this script being run for an extras subpackage? - extras_subpackage = None - if args.package_name and '+' in args.package_name: - # The extras names are encoded in the package names after the + sign. - # We take the part after the rightmost +, ignoring when empty, - # this allows packages like nicotine+ or c++ to work fine. - # While packages with names like +spam or foo+bar would break, - # names started with the plus sign are not very common - # and pluses in the middle can be easily replaced with dashes. - # Python extras names don't contain pluses according to PEP 508. - package_name_parts = args.package_name.rpartition('+') - extras_subpackage = package_name_parts[2].lower() or None - - namespace = (args.namespace + "({})") if args.namespace else "{}" - - for f in (args.files or stdin.readlines()): - f = f.strip() - lower = f.lower() - name = 'python(abi)' - # add dependency based on path, versioned if within versioned python directory - if py_abi and (lower.endswith('.py') or lower.endswith('.pyc') or lower.endswith('.pyo')): - if name not in py_deps: - py_deps[name] = [] - running_python_version = '{}.{}'.format(*version_info[:2]) - purelib = get_path('purelib').split(running_python_version)[0] - platlib = get_path('platlib').split(running_python_version)[0] - for lib in (purelib, platlib): - if lib in f: - spec = ('==', f.split(lib)[1].split(sep)[0]) - if spec not in py_deps[name]: - py_deps[name].append(spec) - - # XXX: hack to workaround RPM internal dependency generator not passing directories - lower_dir = dirname(lower) - if lower_dir.endswith('.egg') or \ - lower_dir.endswith('.egg-info') or \ - lower_dir.endswith('.dist-info'): - lower = lower_dir - f = dirname(f) - # Determine provide, requires, conflicts & recommends based on egg/dist metadata - if lower.endswith('.egg') or \ - lower.endswith('.egg-info') or \ - lower.endswith('.dist-info'): - dist = Distribution(f) - if not dist.py_version: + path_item = f + metadata = FileMetadata(f) + dist = Distribution.from_location(path_item, dist_name, metadata) + # Check if py_version is defined in the metadata file/directory name + if not dist.py_version: + # Try to parse the Python version from the path the metadata + # resides at (e.g. /usr/lib/pythonX.Y/site-packages/...) + import re + res = re.search(r"/python(?P\d+\.\d+)/", path_item) + if res: + dist.py_version = res.group('pyver') + else: warn("Version for {!r} has not been found".format(dist), RuntimeWarning) continue - # If processing an extras subpackage: - # Check that the extras name is declared in the metadata, or - # that there are some dependencies associated with the extras - # name in the requires.txt (this is an outdated way to declare - # extras packages). - # - If there is an extras package declared only in requires.txt - # without any dependencies, this check will fail. In that case - # make sure to use updated metadata and declare the extras - # package there. - if extras_subpackage and extras_subpackage not in dist.extras and not dist.requirements_for_extra(extras_subpackage): - print("*** PYTHON_EXTRAS_NOT_FOUND_ERROR___SEE_STDERR ***") - print(f"\nError: The package name contains an extras name `{extras_subpackage}` that was not found in the metadata.\n" - "Check if the extras were removed from the project. If so, consider removing the subpackage and obsoleting it from another.\n", file=stderr) - exit(65) # os.EX_DATAERR - - if args.majorver_provides or args.majorver_provides_versions or \ - args.majorver_only or args.legacy_provides or args.legacy: - # Get the Python major version - pyver_major = dist.py_version.split('.')[0] - if args.provides: - extras_suffix = f"[{extras_subpackage}]" if extras_subpackage else "" - # If egg/dist metadata says package name is python, we provide python(abi) - if dist.normalized_name == 'python': - name = namespace.format('python(abi)') - if name not in py_deps: - py_deps[name] = [] - py_deps[name].append(('==', dist.py_version)) - if not args.legacy or not args.majorver_only: - if normalized_names_provide_legacy: - name = namespace.format('python{}dist({}{})').format(dist.py_version, dist.legacy_normalized_name, extras_suffix) - if name not in py_deps: - py_deps[name] = [] - if normalized_names_provide_pep503: - name_ = namespace.format('python{}dist({}{})').format(dist.py_version, dist.normalized_name, extras_suffix) - if name_ not in py_deps: - py_deps[name_] = [] - if args.majorver_provides or args.majorver_only or \ - (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions): - if normalized_names_provide_legacy: - pymajor_name = namespace.format('python{}dist({}{})').format(pyver_major, dist.legacy_normalized_name, extras_suffix) - if pymajor_name not in py_deps: - py_deps[pymajor_name] = [] - if normalized_names_provide_pep503: - pymajor_name_ = namespace.format('python{}dist({}{})').format(pyver_major, dist.normalized_name, extras_suffix) - if pymajor_name_ not in py_deps: - py_deps[pymajor_name_] = [] - if args.legacy or args.legacy_provides: - legacy_name = namespace.format('pythonegg({})({})').format(pyver_major, dist.legacy_normalized_name) - if legacy_name not in py_deps: - py_deps[legacy_name] = [] - if dist.version: - version = dist.version - spec = ('==', version) - if args.fail_if_zero: - if RpmVersion(version).is_zero(): - print('*** PYTHON_PROVIDED_VERSION_NORMALIZES_TO_ZERO___SEE_STDERR ***') - print(f'\nError: The version in the Python package metadata {version} normalizes to zero.\n' - 'It\'s likely a packaging error caused by missing version information\n' - '(e.g. when using a version control system snapshot as a source).\n' - 'Try providing the version information manually when building the Python package,\n' - 'for example by setting the SETUPTOOLS_SCM_PRETEND_VERSION environment variable if the package uses setuptools_scm.\n' - 'If you are confident that the version of the Python package is intentionally zero,\n' - 'you may %define the _python_dist_allow_version_zero macro in the spec file to disable this check.\n', file=stderr) - exit(65) # os.EX_DATAERR - - if normalized_names_provide_legacy: - if spec not in py_deps[name]: - py_deps[name].append(spec) - if args.majorver_provides or \ - (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions): - py_deps[pymajor_name].append(spec) - if normalized_names_provide_pep503: - if spec not in py_deps[name_]: - py_deps[name_].append(spec) - if args.majorver_provides or \ - (args.majorver_provides_versions and dist.py_version in args.majorver_provides_versions): - py_deps[pymajor_name_].append(spec) - if args.legacy or args.legacy_provides: - if spec not in py_deps[legacy_name]: - py_deps[legacy_name].append(spec) - if args.requires or (args.recommends and dist.extras): - name = namespace.format('python(abi)') - # If egg/dist metadata says package name is python, we don't add dependency on python(abi) - if dist.normalized_name == 'python': - py_abi = False - if name in py_deps: - py_deps.pop(name) - elif py_abi and dist.py_version: + # XXX: https://github.com/pypa/setuptools/pull/1275 + import platform + platform.python_version = lambda: dist.py_version + + + if Provides_PyMajorVer_Variant or PyMajorVer_Deps or legacy_Provides or legacy: + # Get the Python major version + pyver_major = dist.py_version.split('.')[0] + if Provides: + # If egg/dist metadata says package name is python, we provide python(abi) + if dist.key == 'python': + name = 'python(abi)' + if name not in py_deps: + py_deps[name] = [] + py_deps[name].append(('==', dist.py_version)) + if not legacy or not PyMajorVer_Deps: + name = 'python{}dist({})'.format(dist.py_version, dist.key) + if name not in py_deps: + py_deps[name] = [] + if Provides_PyMajorVer_Variant or PyMajorVer_Deps: + pymajor_name = 'python{}dist({})'.format(pyver_major, dist.key) + if pymajor_name not in py_deps: + py_deps[pymajor_name] = [] + if legacy or legacy_Provides: + legacy_name = 'pythonegg({})({})'.format(pyver_major, dist.key) + if legacy_name not in py_deps: + py_deps[legacy_name] = [] + if dist.version: + version = dist.version + while version.endswith('.0'): + version = version[:-2] + spec = ('==', version) + if spec not in py_deps[name]: + if not legacy: + py_deps[name].append(spec) + if Provides_PyMajorVer_Variant: + py_deps[pymajor_name].append(spec) + if legacy or legacy_Provides: + py_deps[legacy_name].append(spec) + if Requires or (Recommends and dist.extras): + name = 'python(abi)' + # If egg/dist metadata says package name is python, we don't add dependency on python(abi) + if dist.key == 'python': + py_abi = False + if name in py_deps: + py_deps.pop(name) + elif py_abi and dist.py_version: + if name not in py_deps: + py_deps[name] = [] + spec = ('==', dist.py_version) + if spec not in py_deps[name]: + py_deps[name].append(spec) + deps = dist.requires() + if Recommends: + depsextras = dist.requires(extras=dist.extras) + if not Requires: + for dep in reversed(depsextras): + if dep in deps: + depsextras.remove(dep) + deps = depsextras + # console_scripts/gui_scripts entry points need pkg_resources from setuptools + if (dist.get_entry_map('console_scripts') or + dist.get_entry_map('gui_scripts')): + # stick them first so any more specific requirement overrides it + deps.insert(0, Requirement.parse('setuptools')) + # add requires/recommends based on egg/dist metadata + for dep in deps: + if legacy: + name = 'pythonegg({})({})'.format(pyver_major, dep.key) + else: + if PyMajorVer_Deps: + name = 'python{}dist({})'.format(pyver_major, dep.key) + else: + name = 'python{}dist({})'.format(dist.py_version, dep.key) + for spec in dep.specs: + while spec[1].endswith('.0'): + spec = (spec[0], spec[1][:-2]) if name not in py_deps: py_deps[name] = [] - spec = ('==', dist.py_version) if spec not in py_deps[name]: py_deps[name].append(spec) - - if extras_subpackage: - deps = [d for d in dist.requirements_for_extra(extras_subpackage)] - else: - deps = dist.requirements - - # console_scripts/gui_scripts entry points needed pkg_resources from setuptools - # on new Python/setuptools versions, this is no longer required - if nodep_setuptools_pyversion is None or parse(dist.py_version) < nodep_setuptools_pyversion: - if (dist.entry_points and - (lower.endswith('.egg') or - lower.endswith('.egg-info'))): - groups = {ep.group for ep in dist.entry_points} - if {"console_scripts", "gui_scripts"} & groups: - # stick them first so any more specific requirement - # overrides it - deps.insert(0, Requirement('setuptools')) - # add requires/recommends based on egg/dist metadata + if not dep.specs: + py_deps[name] = [] + # Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata + # TODO: implement in rpm later, or...? + if Extras: + deps = dist.requires() + extras = dist.extras + print(extras) + for extra in extras: + print('%%package\textras-{}'.format(extra)) + print('Summary:\t{} extra for {} python package'.format(extra, dist.key)) + print('Group:\t\tDevelopment/Python') + depsextras = dist.requires(extras=[extra]) + for dep in reversed(depsextras): + if dep in deps: + depsextras.remove(dep) + deps = depsextras for dep in deps: - # Even if we're requiring `foo[bar]`, also require `foo` - # to be safe, and to make it discoverable through - # `repoquery --whatrequires` - extras_suffixes = [""] - if args.require_extras_subpackages and dep.extras: - # A dependency can have more than one extras, - # i.e. foo[bar,baz], so let's go through all of them - extras_suffixes += [f"[{e.lower()}]" for e in dep.extras] - - for extras_suffix in extras_suffixes: - if normalized_names_require_pep503: - dep_normalized_name = dep.normalized_name - else: - dep_normalized_name = dep.legacy_normalized_name - - if args.legacy: - name = namespace.format('pythonegg({})({})').format(pyver_major, dep.legacy_normalized_name) + for spec in dep.specs: + if spec[0] == '!=': + print('Conflicts:\t{} {} {}'.format(dep.key, '==', spec[1])) else: - if args.majorver_only: - name = namespace.format('python{}dist({}{})').format(pyver_major, dep_normalized_name, extras_suffix) - else: - name = namespace.format('python{}dist({}{})').format(dist.py_version, dep_normalized_name, extras_suffix) - - if dep.marker and not args.recommends and not extras_subpackage: - if not dep.marker.evaluate(get_marker_env(dist, '')): - continue - + print('Requires:\t{} {} {}'.format(dep.key, spec[0], spec[1])) + print('%%description\t{}'.format(extra)) + print('{} extra for {} python package'.format(extra, dist.key)) + print('%%files\t\textras-{}\n'.format(extra)) + if Conflicts: + # Should we really add conflicts for extras? + # Creating a meta package per extra with recommends on, which has + # the requires/conflicts in stead might be a better solution... + for dep in dist.requires(extras=dist.extras): + name = dep.key + for spec in dep.specs: + if spec[0] == '!=': if name not in py_deps: py_deps[name] = [] - for spec in dep.specifier: - if (spec.operator, spec.version) not in py_deps[name]: - py_deps[name].append((spec.operator, spec.version)) - - # Unused, for automatic sub-package generation based on 'extras' from egg/dist metadata - # TODO: implement in rpm later, or...? - if args.extras: - print(dist.extras) - for extra in dist.extras: - print('%%package\textras-{}'.format(extra)) - print('Summary:\t{} extra for {} python package'.format(extra, dist.legacy_normalized_name)) - print('Group:\t\tDevelopment/Python') - for dep in dist.requirements_for_extra(extra): - for spec in dep.specifier: - if spec.operator == '!=': - print('Conflicts:\t{} {} {}'.format(dep.legacy_normalized_name, '==', spec.version)) - else: - print('Requires:\t{} {} {}'.format(dep.legacy_normalized_name, spec.operator, spec.version)) - print('%%description\t{}'.format(extra)) - print('{} extra for {} python package'.format(extra, dist.legacy_normalized_name)) - print('%%files\t\textras-{}\n'.format(extra)) - if args.conflicts: - # Should we really add conflicts for extras? - # Creating a meta package per extra with recommends on, which has - # the requires/conflicts in stead might be a better solution... - for dep in dist.requirements: - for spec in dep.specifier: - if spec.operator == '!=': - if dep.legacy_normalized_name not in py_deps: - py_deps[dep.legacy_normalized_name] = [] - spec = ('==', spec.version) - if spec not in py_deps[dep.legacy_normalized_name]: - py_deps[dep.legacy_normalized_name].append(spec) - - for name in sorted(py_deps): - if py_deps[name]: - # Print out versioned provides, requires, recommends, conflicts - spec_list = [] - for spec in py_deps[name]: - spec_list.append(convert(name, spec[0], spec[1])) - if len(spec_list) == 1: - print(spec_list[0]) + spec = ('==', spec[1]) + if spec not in py_deps[name]: + py_deps[name].append(spec) +names = list(py_deps.keys()) +names.sort() +for name in names: + if py_deps[name]: + # Print out versioned provides, requires, recommends, conflicts + for spec in py_deps[name]: + if spec[0] == '!=': + print('({n} < {v} or {n} >= {v}.0)'.format(n=name, v=spec[1])) else: - # Sort spec_list so that the results can be tested easily - print('({})'.format(' with '.join(sorted(spec_list)))) - else: - # Print out unversioned provides, requires, recommends, conflicts - print(name) - - -if __name__ == "__main__": - """To allow this script to be importable (and its classes/functions - reused), actions are performed only when run as a main script.""" - try: - main() - except Exception as exc: - print("*** PYTHONDISTDEPS_GENERATORS_FAILED ***", flush=True) - raise RuntimeError("Error: pythondistdeps.py generator encountered an unhandled exception and was terminated.") from exc - + print('{} {} {}'.format(name, spec[0], spec[1])) + else: + # Print out unversioned provides, requires, recommends, conflicts + print(name) diff --git a/pythonname.attr b/pythonname.attr deleted file mode 100644 index 1716d43e585e4bf12778669bf1af17f6297c9c37..0000000000000000000000000000000000000000 --- a/pythonname.attr +++ /dev/null @@ -1,18 +0,0 @@ -%__pythonname_provides() %{lua: - local python = require 'fedora.srpm.python' - local name = rpm.expand('%{name}') - local evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}') - local provides = python.python_altprovides_once(name, evr) - -- provides is either an array/table or nil - -- nil means the function was already called with the same arguments: - -- either with another file in %1 or manually via %py_provides - if provides then - for i, provide in ipairs(provides) do - print(provide .. ' ') - end - end -} - -%__pythonname_obsoletes() %{nil} - -%__pythonname_path ^/