diff --git a/.python-rpm-generators.metadata b/.python-rpm-generators.metadata deleted file mode 100644 index e69de29..0000000 diff --git a/SOURCES/COPYING b/COPYING similarity index 100% rename from SOURCES/COPYING rename to COPYING diff --git a/SOURCES/python.attr b/SOURCES/python.attr deleted file mode 100644 index d1fb80d..0000000 --- a/SOURCES/python.attr +++ /dev/null @@ -1,4 +0,0 @@ -%__python_provides %{_rpmconfigdir}/pythondistdeps.py --provides --majorver-provides-versions @MAJORVER-PROVIDES-VERSIONS@ -%__python_requires %{_rpmconfigdir}/pythondeps.sh --requires -%__python_path ^((/usr/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/site-packages/[^/]+\\.(dist-info|egg-info|egg-link))|(/usr/lib(64)?/python[[:digit:]]+\\.[[:digit:]]+/.*\\.(py[oc]?|so))|(%{_bindir}/python[[:digit:]]+\\.[[:digit:]]+))$ -%__python_magic [Pp]ython.*(executable|byte-compiled) diff --git a/SOURCES/pythondeps.sh b/SOURCES/pythondeps.sh deleted file mode 100755 index 8884f4e..0000000 --- a/SOURCES/pythondeps.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/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 -E "/usr/lib[^/]*/python[[:digit:]]+\.[[:digit:]]+/.*" \ - | sed -Ee "s|.*/usr/lib[^/]*/python([[:digit:]]+\.[[:digit:]]+)/.*|python(abi) = \1|g" \ - | sort | uniq - ;; -esac - -exit 0 diff --git a/SOURCES/pythondistdeps.py b/SOURCES/pythondistdeps.py deleted file mode 100755 index 01f35b3..0000000 --- a/SOURCES/pythondistdeps.py +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/libexec/platform-python -# -*- coding: utf-8 -*- -# -# Copyright 2010 Per Øyvind Karlsen -# Copyright 2015 Neal Gompa -# -# This program is free software. It may be redistributed and/or modified under -# the terms of the LGPL version 2.1 (or later). -# -# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data -# - -from __future__ import print_function -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 - - -opts, args = getopt( - argv[1:], 'hPRrCEMmLl:', - ['help', 'provides', 'requires', 'recommends', 'conflicts', 'extras', 'majorver-provides', 'majorver-provides-versions=', 'majorver-only', 'legacy-provides' , 'legacy']) - -Provides = False -Requires = False -Recommends = False -Conflicts = False -Extras = False -Provides_PyMajorVer_Variant = False -Provides_PyMajorVer_Versions = None -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 for all Python versions') - print(' --majorver-provides-versions VERSIONS\n' - ' \tPrint extra Provides with Python major version only for listed Python VERSIONS (comma separated, no spaces, e.g. 2.7,3.6)') - 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 ('--majorver-provides-versions'): - Provides_PyMajorVer_Versions = a.split(",") - 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 Provides_PyMajorVer_Variant and Provides_PyMajorVer_Versions: - print("Error, options --majorver-provides and --majorver-provides-versions are mutually incompatible.") - exit(2) - -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 - dist_name = basename(f) - if isdir(f): - path_item = dirname(f) - metadata = PathMetadata(path_item, f) - else: - 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 - - # 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 or Provides_PyMajorVer_Versions: - # 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 or \ - (Provides_PyMajorVer_Versions and dist.py_version in Provides_PyMajorVer_Versions): - 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: - spec = ('==', dist.version) - if spec not in py_deps[name]: - if not legacy: - py_deps[name].append(spec) - if Provides_PyMajorVer_Variant or \ - (Provides_PyMajorVer_Versions and dist.py_version in Provides_PyMajorVer_Versions): - 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 - # 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: - if spec[0] != '!=': - if name not in py_deps: - py_deps[name] = [] - if spec not in py_deps[name]: - py_deps[name].append(spec) - 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: - for spec in dep.specs: - if spec[0] == '!=': - print('Conflicts:\t{} {} {}'.format(dep.key, '==', spec[1])) - else: - 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] = [] - 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]: - print('{} {} {}'.format(name, spec[0], spec[1])) - else: - # Print out unversioned provides, requires, recommends, conflicts - print(name) diff --git a/SPECS/python-rpm-generators.spec b/SPECS/python-rpm-generators.spec deleted file mode 100644 index 35846ef..0000000 --- a/SPECS/python-rpm-generators.spec +++ /dev/null @@ -1,118 +0,0 @@ -# Disable automatic bytecompilation. We install only one script and we will -# never "import" it. -%undefine py_auto_byte_compile - -Name: python-rpm-generators -Summary: Dependency generators for Python RPMs -Version: 5 -Release: 8%{?dist} - -# Originally all those files were part of RPM, so license is kept here -License: GPLv2+ -Url: https://src.fedoraproject.org/python-rpm-generators -# Commit is the last change in following files -Source0: https://raw.githubusercontent.com/rpm-software-management/rpm/102eab50b3d0d6546dfe082eac0ade21e6b3dbf1/COPYING -Source1: python.attr -Source2: pythondeps.sh -Source3: pythondistdeps.py - -BuildArch: noarch - -%description -%{summary}. - -%package -n python3-rpm-generators -Summary: %{summary} -%if 0%{?rhel} && 0%{?rhel} >= 8 -Requires: platform-python-setuptools -%else -Requires: python3-setuptools -%endif -# The point of split -Conflicts: rpm-build < 4.13.0.1-2 - -%description -n python3-rpm-generators -%{summary}. - -%prep -%autosetup -c -T -cp -a %{sources} . - -# Set which Python versions should have the major-version provides -# (pythonXdist...) generated -sed -i 's/@MAJORVER-PROVIDES-VERSIONS@/2.7,3.6/' python.attr - -%install -install -Dpm0644 -t %{buildroot}%{_fileattrsdir} python.attr -install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} pythondeps.sh pythondistdeps.py - -%files -n python3-rpm-generators -%license COPYING -%{_fileattrsdir}/python.attr -%{_rpmconfigdir}/pythondeps.sh -%{_rpmconfigdir}/pythondistdeps.py - -%changelog -* Mon Nov 14 2022 Charalampos Stratakis - 5-8 -- Fix the pythondeps.sh and pythondistdeps.py scripts for multiple digits python versions -- Resolves: rhbz#2143990 - -* Tue Jun 15 2021 Tomas Orsava - 5-7 -- Do not parse nested dist/egg-info metadata -- Resolves: rhbz#1916172 - -* Thu Dec 12 2019 Tomas Orsava - 5-6 -- Enabled gating -- Related: rhbz#1776941 - -* Wed Nov 27 2019 Tomas Orsava - 5-5 -- Create major-version provides only on major Python versions (2.7, 3.6) -- Fix an extra parenthesis in python.attr -- Resolves: rhbz#1776941 - -* Fri Nov 16 2018 Lumír Balhar - 5-4 -- Require platform-python-setuptools instead of python3-setuptools -- Resolves: rhbz#1650544 - -* Sat Jul 28 2018 Miro Hrončok - 5-3 -- Use nonstandardlib for purelib definition (#1609492) - -* Tue Jun 05 2018 Tomas Orsava - 5-2 -- Switch the pythondistdeps.py script to /usr/libexec/platform-python - -* Sun Feb 11 2018 Igor Gnatenko - 5-1 -- Fork upstream generators -- "Fix" support of environment markers - -* Fri Feb 09 2018 Fedora Release Engineering - 4.14.0-2.1 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild - -* Tue Nov 28 2017 Tomas Orsava - 4.14.0-2 -- Switch bootsrapping macro to a bcond for modularity - -* Fri Oct 20 2017 Tomas Orsava - 4.14.0-1 -- Rebase to rpm 4.14.0 final (http://rpm.org/wiki/Releases/4.14.0) -- Re-synchronize version/release macros with the rpm Fedora package - -* Mon Sep 18 2017 Tomas Orsava - 4.14.0-0.rc1.1 -- Update to a new upstream version of RPM -- Drop upstreamed patches -- Renumber remaining patches - -* Thu Aug 24 2017 Miro Hrončok - 4.13.0.1-4 -- Add patch 10: Do not provide pythonXdist for platform-python packages (rhbz#1484607) - -* Tue Aug 08 2017 Tomas Orsava - 4.13.0.1-3 -- Add patch 9: Generate requires and provides for platform-python(abi) - (https://fedoraproject.org/wiki/Changes/Platform_Python_Stack) - -* Thu Jul 27 2017 Fedora Release Engineering - 4.13.0.1-2.1 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild - -* Thu May 18 2017 Tomas Orsava - 4.13.0.1-2 -- Added a license file -- Added a dependency on rpm for the proper directory structure -- Properly owning the __pycache__ directory - -* Tue May 02 2017 Tomas Orsava - 4.13.0.1-1 -- Splitting Python RPM generators from the `rpm` package to standalone one diff --git a/python-rpm-generators.spec b/python-rpm-generators.spec new file mode 100644 index 0000000..e59d3f1 --- /dev/null +++ b/python-rpm-generators.spec @@ -0,0 +1,292 @@ +Name: python-rpm-generators +Summary: Dependency generators for Python RPMs +Version: 14 +Release: 12%{?dist} + +Url: https://src.fedoraproject.org/rpms/python-rpm-generators + +# Originally the following files were part of RPM, so the license is inherited: GPL-2.0-or-later +# The COPYING file is grabbed from the last commit that changed the files +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: +# LicenseRef-Fedora-Public-Domain OR CC0-1.0 OR LGPL-2.1-or-later OR GPL-2.0-or-later +# Note that CC0-1.0 is not allowed for code in Fedora, so we skip it in the package License tag +Source5: pythonbundles.py + +# See individual licenses above Source declarations +# Originally, this was simplified to GPL-2.0-or-later, but "effective license" analysis is no longer allowed +License: GPL-2.0-or-later AND LGPL-2.1-or-later AND (LicenseRef-Fedora-Public-Domain OR LGPL-2.1-or-later OR GPL-2.0-or-later) + +BuildArch: noarch + +%description +%{summary}. + +%package -n python3-rpm-generators +Summary: %{summary} +Requires: python3-packaging +# We have parametric macro generators, we need RPM 4.16 (4.15.90+ is 4.16 alpha) +Requires: rpm > 4.15.90-0 +# This contains the Lua functions we use: +Requires: python-srpm-macros >= 3.10-15 + +%description -n python3-rpm-generators +%{summary}. + +%prep +%autosetup -c -T +cp -a %{sources} . + +%install +install -Dpm0644 -t %{buildroot}%{_fileattrsdir} *.attr +install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} *.py + +%files -n python3-rpm-generators +%license COPYING +%{_fileattrsdir}/python.attr +%{_fileattrsdir}/pythondist.attr +%{_fileattrsdir}/pythonname.attr +%{_rpmconfigdir}/pythondistdeps.py +%{_rpmconfigdir}/pythonbundles.py + +%changelog +* Tue Oct 29 2024 Troy Dawson - 14-12 +- Bump release for October 2024 mass rebuild: + Resolves: RHEL-64018 + +* Mon Jun 24 2024 Troy Dawson - 14-11 +- Bump release for June 2024 mass rebuild + +* Fri Jan 26 2024 Fedora Release Engineering - 14-10 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Mon Jan 22 2024 Fedora Release Engineering - 14-9 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_40_Mass_Rebuild + +* Tue Oct 03 2023 Miro Hrončok - 14-8 +- Avoid DeprecationWarning: Implicit None on return values is deprecated and will raise KeyErrors + +* Fri Jul 21 2023 Fedora Release Engineering - 14-7 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_39_Mass_Rebuild + +* Wed May 24 2023 Todd Zullinger - 14-6 +- Fix URL tag + +* Fri May 05 2023 Miro Hrončok - 14-5 +- Declare the license via a complex SPDX expression rather than "effective license" + +* Mon Apr 17 2023 Kalev Lember - 14-4 +- Generate provides for /app-installed flatpak builds + +* Tue Mar 07 2023 Miro Hrončok - 14-3 +- Avoid needless pkg_resources import in pythonbundles.py +- Ignore environment markers in pythonbundles.py + +* Fri Jan 20 2023 Fedora Release Engineering - 14-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_38_Mass_Rebuild + +* Thu Dec 22 2022 Karolina Surma - 14-1 +- https://fedoraproject.org/wiki/Changes/Prevent-Providing-python3dist(pkg)=0 + +* Fri Jul 22 2022 Fedora Release Engineering - 13-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_37_Mass_Rebuild + +* Thu Jun 02 2022 Miro Hrončok - 13-1 +- https://fedoraproject.org/wiki/Changes/PythonDistPEP503ProvidesOnly + +* Fri May 27 2022 Miro Hrončok - 12-15 +- Don't include all requirements with True-evaluating markers in extras subpackages +- Fixes: rhbz#2090186 + +* Thu Feb 10 2022 Sandro Mani - 12-14 +- Add namespace option to pythodistdeps.py to allow mingw-python generatros + +* Wed Jan 26 2022 Tomas Orsava - 12-13 +- From `python3-foo` packages automatically generate `python3.X-foo` Obsoletes + tags on CentOS/RHEL + +* Fri Jan 21 2022 Fedora Release Engineering - 12-12 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_36_Mass_Rebuild + +* Sun Dec 19 2021 Gordon Messmer - 12-11 +- Handle legacy version specifiers that would previously raise exceptions. + +* Fri Oct 29 2021 Gordon Messmer - 12-10 +- Additional fix for dev releases. + +* Thu Oct 28 2021 Gordon Messmer - 12-9 +- Sync dependency conversion with upstream pyreq2rpm. +- Improve handling of > and < operators, and != operator with prefix matching + +* Fri Jul 23 2021 Fedora Release Engineering - 12-8 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild + +* Tue May 25 2021 Tomas Orsava - 12-7 +- pythondistdeps.py: Detect missing or corrupted metadata +- pythondistdeps.py: Catch all exceptions and terminate the build if one is raised + +* Mon Apr 19 2021 Miro Hrončok - 12-6 +- Get rid of distutils deprecation warning (by not using it) +- The distutils module is deprecated in Python 3.10+ +- https://www.python.org/dev/peps/pep-0632/ + +* Wed Mar 31 2021 Miro Hrončok - 12-5 +- Do not generate setuptools requirement for console_scripts on Python 3.10+ +- See https://fedoraproject.org/wiki/Changes/Reduce_dependencies_on_python3-setuptools + +* Thu Mar 11 2021 Tomas Orsava - 12-4 +- scripts/pythondistdeps: Treat extras names case-insensitively and always + output them in lower case (#1936875) + +* Mon Feb 22 2021 Tomas Orsava - 12-3 +- scripts/pythondistdeps: Fix for Python 3.10 + +* Wed Feb 17 2021 Tomas Orsava - 12-2 +- scripts/pythondistdeps: Switch from using pkg_resources to importlib.metadata + for reading the egg/dist-info metadata +- The script no longer requires setuptools but instead requires packaging + +* Wed Feb 03 2021 Miro Hrončok - 12-1 +- Disable the dist generators for Python 2 +- https://fedoraproject.org/wiki/Changes/Disable_Python_2_Dist_RPM_Generators_and_Freeze_Python_2_Macros + +* Wed Jan 27 2021 Fedora Release Engineering - 11-13 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_34_Mass_Rebuild + +* Mon Oct 19 2020 Tomas Orsava - 11-12 +- Run scripts in an isolated Python environment (#1889080) + +* Wed Jul 29 2020 Fedora Release Engineering - 11-11 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_33_Mass_Rebuild + +* Tue Jul 21 2020 Miro Hrončok - 11-10 +- pythondistdeps: Split Python Extras names after the rightmost plus sign +- pythondistdeps: Handle edge cases of version comparisons more closely to + upstream, despite irrationality + See: https://github.com/pypa/packaging/issues/320 + +* Fri Jul 10 2020 Tomas Orsava - 11-9 +- pythondistdeps: Implement provides/requires for extras packages +- Enable --require-extras-subpackages +- Adapt Python version marker workaround for setuptools 42+ + +* Fri Jun 26 2020 Miro Hrončok - 11-8 +- Fix python(abi) requires generator, it picked files from almost good directories +- Add a script to generate Python bundled provides + +* Thu May 21 2020 Miro Hrončok - 11-7 +- Use PEP 503 names for requires + +* Tue May 05 2020 Miro Hrončok - 11-6 +- Deduplicate automatically provided names trough Python RPM Lua macros + +* Wed Apr 29 2020 Tomas Orsava - 11-5 +- Backporting proposed upstream changes + https://github.com/rpm-software-management/rpm/pull/1195 + - Only provide python3dist(..) for the main Python versions (BZ#1812083) + - Preparation for the proper handling of normalized names (BZ#1791530) + - Add a test suite (and enable it in Fedora CI) + - Better error messages for unsupported package versions + - Fix sorting of dev versions + +* Tue Apr 28 2020 Miro Hrončok - 11-4 +- Don't define global Lua variables from Python generator + +* Mon Apr 20 2020 Gordon Messmer - 11-3 +- Handle all-zero versions without crashing + +* Tue Apr 07 2020 Miro Hrončok - 11-2 +- Use dynamic %%_prefix value when matching files for python(abi) provides +- Sync with upstream RPM dist generator + +* Wed Apr 01 2020 Miro Hrončok - 11-1 +- Rewrite python(abi) generators to Lua to make them faster +- RPM 4.16+ is needed +- Automatically call %%python_provide + +* Thu Jan 30 2020 Fedora Release Engineering - 10-4 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_32_Mass_Rebuild + +* Fri Jan 17 2020 Miro Hrončok - 10-3 +- Also provide pythonXdist() with PEP 503 normalized names (#1791530) + +* Fri Jan 03 2020 Miro Hrončok - 10-2 +- Fix more complicated requirement expressions by adding parenthesis + +* Wed Jan 01 2020 Miro Hrončok - 10-1 +- Handle version ending with ".*" (#1758141) +- Handle compatible-release operator "~=" (#1758141) +- Use rich deps for semantically versioned dependencies +- Match Python version if minor has multiple digits (e.g. 3.10, #1777382) +- Only add setuptools requirement for egg-info packages + +* Fri Jul 26 2019 Fedora Release Engineering - 9-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Mon Jun 24 2019 Tomas Orsava - 9-1 +- Canonicalize Python versions and properly handle != spec + +* Wed Apr 17 2019 Miro Hrončok - 8-1 +- console_scripts entry points to require setuptools + https://github.com/rpm-software-management/rpm/pull/666 + +* Sat Feb 02 2019 Fedora Release Engineering - 7-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Thu Dec 20 2018 Igor Gnatenko - 7-1 +- Enable requires generator + +* Wed Oct 03 2018 Igor Gnatenko - 6-1 +- Tighten regex for depgen + +* Sat Jul 28 2018 Miro Hrončok - 5-4 +- Use nonstandardlib for purelib definition (#1609492) + +* Sat Jul 28 2018 Igor Gnatenko - 5-3 +- Add pythondist generator + +* Sat Jul 14 2018 Fedora Release Engineering - 5-2 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_29_Mass_Rebuild + +* Sun Feb 11 2018 Igor Gnatenko - 5-1 +- Fork upstream generators +- "Fix" support of environment markers + +* Fri Feb 09 2018 Fedora Release Engineering - 4.14.0-2.1 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_28_Mass_Rebuild + +* Tue Nov 28 2017 Tomas Orsava - 4.14.0-2 +- Switch bootsrapping macro to a bcond for modularity + +* Fri Oct 20 2017 Tomas Orsava - 4.14.0-1 +- Rebase to rpm 4.14.0 final (http://rpm.org/wiki/Releases/4.14.0) +- Re-synchronize version/release macros with the rpm Fedora package + +* Mon Sep 18 2017 Tomas Orsava - 4.14.0-0.rc1.1 +- Update to a new upstream version of RPM +- Drop upstreamed patches +- Renumber remaining patches + +* Thu Aug 24 2017 Miro Hrončok - 4.13.0.1-4 +- Add patch 10: Do not provide pythonXdist for platform-python packages (rhbz#1484607) + +* Tue Aug 08 2017 Tomas Orsava - 4.13.0.1-3 +- Add patch 9: Generate requires and provides for platform-python(abi) + (https://fedoraproject.org/wiki/Changes/Platform_Python_Stack) + +* Thu Jul 27 2017 Fedora Release Engineering - 4.13.0.1-2.1 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_27_Mass_Rebuild + +* Thu May 18 2017 Tomas Orsava - 4.13.0.1-2 +- Added a license file +- Added a dependency on rpm for the proper directory structure +- Properly owning the __pycache__ directory + +* Tue May 02 2017 Tomas Orsava - 4.13.0.1-1 +- Splitting Python RPM generators from the `rpm` package to standalone one diff --git a/python.attr b/python.attr new file mode 100644 index 0000000..cf5ae39 --- /dev/null +++ b/python.attr @@ -0,0 +1,31 @@ +%__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:]]+))$ diff --git a/pythonbundles.py b/pythonbundles.py new file mode 100755 index 0000000..b0e5ecf --- /dev/null +++ b/pythonbundles.py @@ -0,0 +1,94 @@ +#!/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 new file mode 100644 index 0000000..ede3a51 --- /dev/null +++ b/pythondist.attr @@ -0,0 +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)$ diff --git a/pythondistdeps.py b/pythondistdeps.py new file mode 100755 index 0000000..b43ed39 --- /dev/null +++ b/pythondistdeps.py @@ -0,0 +1,626 @@ +#!/usr/bin/python3 -s +# -*- 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). +# +# RPM python dependency generator, using .egg-info/.egg-link/.dist-info data +# + +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 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) + ':' + 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: + 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: + 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 + 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) + 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 + + 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]) + 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 + diff --git a/pythonname.attr b/pythonname.attr new file mode 100644 index 0000000..205570a --- /dev/null +++ b/pythonname.attr @@ -0,0 +1,36 @@ +%__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() %{?rhel:%{lua: + -- On CentOS/RHEL we automatically generate Obsoletes tags in the form: + -- package python3-foo -> Obsoletes: python3.XY-foo + -- This provides a clean upgrade path between major versions of CentOS/RHEL. + -- In Fedora this is not needed as we don't ship ecosystem packages + -- for alternative Python interpreters. + local python = require 'fedora.srpm.python' + local name = rpm.expand('%{name}') + local evr = rpm.expand('%{?epoch:%{epoch}:}%{version}-%{release}') + local obsoletes = python.python_altobsoletes_once(name, evr) + -- obsoletes 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 obsoletes then + for i, obsolete in ipairs(obsoletes) do + print(obsolete .. ' ') + end + end +}} + +%__pythonname_path ^/