diff --git a/python-rpm-generators.spec b/python-rpm-generators.spec index f18b0e1..e21a6b3 100644 --- a/python-rpm-generators.spec +++ b/python-rpm-generators.spec @@ -1,8 +1,8 @@ %undefine py_auto_byte_compile Name: python-rpm-generators -Version: 12 -Release: 1 +Version: 9 +Release: 2 Summary: Dependency generators for Python RPMs License: GPLv2+ @@ -10,9 +10,8 @@ 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 -Source3: pythonname.attr +Source3: pythondeps.sh Source4: pythondistdeps.py -Source5: pythonbundles.py BuildArch: noarch @@ -33,19 +32,21 @@ 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 +* Fri Jan 07 2022 shixuantong - 9-2 +- downgrade version to 9 + * Thu Dec 09 2021 liudabo - 12-1 - upgrade version to 12 diff --git a/python.attr b/python.attr index 1793d3c..f5d2dff 100644 --- a/python.attr +++ b/python.attr @@ -1,27 +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') - if path:match('/usr/bin/python%d+%.%d+$') then - local provides = path:gsub('.*/usr/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') - if path:match('/usr/lib%d*/python%d+%.%d+/.*') then - local requires = path:gsub('.*/usr/lib%d*/python(%d+%.%d+)/.*', 'python(abi) = %1') - print(requires) - end -} - -%__python_path ^((%{_prefix}/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 100755 index 6242e20..0000000 --- a/pythonbundles.py +++ /dev/null @@ -1,91 +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 is more permissive. -# -# 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 - -# inject parse_version import to pythondistdeps -# not the nicest API, but :/ -from pkg_resources import parse_version -import pythondistdeps -pythondistdeps.parse_version = parse_version - - -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: - name, _, version = line.partition('==') - name = pythondistdeps.normalize_name(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/pythondeps.sh b/pythondeps.sh new file mode 100644 index 0000000..10a060a --- /dev/null +++ b/pythondeps.sh @@ -0,0 +1,32 @@ +#!/bin/bash + +[ $# -ge 1 ] || { + cat > /dev/null + exit 0 +} + +case $1 in +-P|--provides) + shift + # 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) + grep "/usr/bin/python.\..$" \ + | sed -e "s|.*/usr/bin/python\(.\..\)|python(abi) = \1|" + ;; +-R|--requires) + shift + # Match buildroot paths of the form + # /PATH/OF/BUILDROOT/usr/lib/pythonMAJOR.MINOR/ and + # /PATH/OF/BUILDROOT/usr/lib64/pythonMAJOR.MINOR/ + # generating (uniqely) lines of the form: + # python(abi) = MAJOR.MINOR + grep "/usr/lib[^/]*/python.\../.*" \ + | sed -e "s|.*/usr/lib[^/]*/python\(.\..\)/.*|python(abi) = \1|g" \ + | sort | uniq + ;; +esac + +exit 0 diff --git a/pythondist.attr b/pythondist.attr index 747cc32..2bf737a 100644 --- a/pythondist.attr +++ b/pythondist.attr @@ -1,3 +1,3 @@ -%__pythondist_provides %{_rpmconfigdir}/pythondistdeps.py --provides --normalized-names-format pep503 --package-name %{name} --normalized-names-provide-both --majorver-provides-versions %{__default_python3_version} -%__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 ^/usr/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 old mode 100755 new mode 100644 index 2e4c9c3..1d3535b --- 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,572 +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 +opts, args = getopt( + argv[1:], 'hPRrCEMmLl:', + ['help', 'provides', 'requires', 'recommends', 'conflicts', 'extras', 'majorver-provides', 'majorver-only', 'legacy-provides' , 'legacy']) -try: - from importlib.metadata import PathDistribution -except ImportError: - from importlib_metadata import PathDistribution +Provides = False +Requires = False +Recommends = False +Conflicts = False +Extras = False +Provides_PyMajorVer_Variant = False +PyMajorVer_Deps = False +legacy_Provides = False +legacy = False -try: - from pathlib import Path -except ImportError: - from pathlib2 import Path +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() -def normalize_name(name): - """https://www.python.org/dev/peps/pep-0503/#normalized-names""" - return re.sub(r'[-_.]+', '-', name).lower() +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) - -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.""" - return self.metadata['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 = [] - for req in self.requirements: - if not req.marker: - continue - if req.marker.evaluate(get_marker_env(self, extra)): - 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 + # 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: - 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 increment(self): - self.version[-1] += 1 - self.pre = None - self.dev = None - self.post = None - return self - - def __str__(self): - if isinstance(self.version, str): - return self.version - if self.epoch: - rpm_epoch = str(self.epoch) + ':' - 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 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) - 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) - # 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('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() - - 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 - - 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 + # XXX: https://github.com/pypa/setuptools/pull/1275 + import platform + platform.python_version = lambda: dist.py_version - 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 = '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 = '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_ = '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 = '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_ = '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 = '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 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): + 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 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: + 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 + for spec in dep.specs: + if spec[0] == '!=': + print('Conflicts:\t{} {} {}'.format(dep.key, '==', spec[1])) else: - dep_normalized_name = dep.legacy_normalized_name - - if args.legacy: - name = 'pythonegg({})({})'.format(pyver_major, dep.legacy_normalized_name) - else: - if args.majorver_only: - name = 'python{}dist({}{})'.format(pyver_major, dep_normalized_name, extras_suffix) - else: - name = '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 85969d7..0000000 --- a/pythonname.attr +++ /dev/null @@ -1,19 +0,0 @@ -%__pythonname_provides() %{lua: - local python = require 'fedora.srpm.python' - -- this macro is called for each file in a package, the path being in %1 - -- but we don't need to know the path, so we would get for each file: Macro %1 defined but not used within scope - -- in here, we expand %name conditionally on %1 to suppress the warning - local name = rpm.expand('%{?1:%{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_provide - if provides then - for i, provide in ipairs(provides) do - print(provide .. ' ') - end - end -} - -%__pythonname_path ^/