diff --git a/macros.python-rpm-generators b/macros.python-rpm-generators new file mode 100644 index 0000000..59e0557 --- /dev/null +++ b/macros.python-rpm-generators @@ -0,0 +1,6 @@ +# Enable purl(pkg:pypi/...) Provides generation by default. +# To disable for packages not actually on PyPI, add to the spec file: +# %%undefine _purl_provides_pypi +# or: +# %%global _purl_provides_pypi 0 +%_purl_provides_pypi 1 diff --git a/python-rpm-generators.spec b/python-rpm-generators.spec index 0e080a7..4a82d2d 100644 --- a/python-rpm-generators.spec +++ b/python-rpm-generators.spec @@ -1,6 +1,6 @@ Name: python-rpm-generators Summary: Dependency generators for Python RPMs -Version: 14 +Version: 15 Release: %autorelease Url: https://src.fedoraproject.org/rpms/python-rpm-generators @@ -18,6 +18,11 @@ Source4: pythondistdeps.py # 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 +# Macros to enable/disable opt-in/opt-out generator features. +# Older features have their macros in python-srpm-macros, +# but shipping them here is simpler to maintain. +# This is essentially data, so no explicit license. +Source6: macros.python-rpm-generators # See individual licenses above Source declarations # Originally, this was simplified to GPL-2.0-or-later, but "effective license" analysis is no longer allowed @@ -46,6 +51,7 @@ cp -a %{sources} . %install install -Dpm0644 -t %{buildroot}%{_fileattrsdir} *.attr install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} *.py +install -Dpm0644 -t %{buildroot}%{_rpmmacrodir} macros.* %files -n python3-rpm-generators %license COPYING @@ -54,6 +60,7 @@ install -Dpm0755 -t %{buildroot}%{_rpmconfigdir} *.py %{_fileattrsdir}/pythonname.attr %{_rpmconfigdir}/pythondistdeps.py %{_rpmconfigdir}/pythonbundles.py +%{_rpmmacrodir}/macros.python-rpm-generators %changelog %autochangelog diff --git a/pythondist.attr b/pythondist.attr index ede3a51..c6e6f49 100644 --- a/pythondist.attr +++ b/pythondist.attr @@ -1,3 +1,3 @@ -%__pythondist_provides %{_rpmconfigdir}/pythondistdeps.py --provides --normalized-names-format pep503 --package-name %{name} --majorver-provides-versions %{__default_python3_version} %{?!_python_dist_allow_version_zero:--fail-if-zero} +%__pythondist_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} %[0%{?_purl_provides_pypi} ? "--purl-provides" : ""] %__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 index a982d9f..a7b97d9 100755 --- a/pythondistdeps.py +++ b/pythondistdeps.py @@ -20,6 +20,7 @@ from sysconfig import get_path from warnings import warn from packaging.requirements import Requirement as Requirement_ +from packaging.utils import canonicalize_version from packaging.version import parse import packaging.markers @@ -63,6 +64,13 @@ def legacy_normalize_name(name): return re.sub(r'[-_]+', '-', name).lower() +def purl_normalize_name(name): + """https://packageurl.org/docs/purl-spec/types/definitions/pypi-definition#name-definition + Lowercase and replace underscore _ with dash -. + Unlike PEP 503, dots and sequences of dashes are preserved.""" + return name.lower().replace('_', '-') + + class Requirement(Requirement_): def __init__(self, requirement_string): super(Requirement, self).__init__(requirement_string) @@ -343,12 +351,17 @@ def main(): 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('--purl-provides', action='store_true', + help='Generate purl(pkg:pypi/...) Provides for Package URL identification') 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') + if args.purl_provides and not args.provides: + raise parser.error('--purl-provides only works with --provides') + py_abi = args.requires py_deps = {} @@ -479,6 +492,20 @@ def main(): legacy_name = namespace.format('pythonegg({})({})').format(pyver_major, dist.legacy_normalized_name) if legacy_name not in py_deps: py_deps[legacy_name] = [] + if args.purl_provides and not extras_subpackage and dist.name and dist.version: + # The purl is a universal identifier, not namespace-wrapped. + # The version is canonicalized per PEP 440 but trailing + # zeros are preserved to match how versions appear on PyPI. + # The purl spec does not specify PEP 440 canonicalization, + # but without it, non-canonical spellings like "1.0beta2" + # and "1.0b2" would produce different purls for the same + # package version. + purl_name = purl_normalize_name(dist.name) + purl_version = canonicalize_version(dist.version, strip_trailing_zero=False) + purl_provide = 'purl(pkg:pypi/{}@{})'.format(purl_name, purl_version) + if purl_provide not in py_deps: + # The Provide is unversioned, version is part of name + py_deps[purl_provide] = [] if dist.version: version = dist.version spec = ('==', version) diff --git a/tests/data/scripts_pythondistdeps/test-data.yaml b/tests/data/scripts_pythondistdeps/test-data.yaml index aa0bcdc..271a6be 100644 --- a/tests/data/scripts_pythondistdeps/test-data.yaml +++ b/tests/data/scripts_pythondistdeps/test-data.yaml @@ -968,9 +968,28 @@ python3dist(backports.range) = 3.7.2 requires: python(abi) = 3.7 --requires --normalized-names-format pep503: - --provides --majorver-provides --normalized-names-format pep503: + --provides --majorver-provides --normalized-names-format pep503 --purl-provides: + usr/lib/python2.7/site-packages/kubernetes-11.0.0b2.dist-info: + provides: |- + purl(pkg:pypi/kubernetes@11.0.0b2) + python2.7dist(kubernetes) = 11~b2 + python2dist(kubernetes) = 11~b2 + requires: |- + python(abi) = 2.7 + python2.7dist(certifi) >= 14.5.14 + python2.7dist(google-auth) >= 1.0.1 + python2.7dist(ipaddress) >= 1.0.17 + python2.7dist(python-dateutil) >= 2.5.3 + python2.7dist(pyyaml) >= 3.12 + python2.7dist(requests) + python2.7dist(requests-oauthlib) + python2.7dist(setuptools) >= 21 + python2.7dist(six) >= 1.9 + python2.7dist(urllib3) >= 1.24.2 + ((python2.7dist(websocket-client) < 0.40 or python2.7dist(websocket-client) > 0.40) with (python2.7dist(websocket-client) < 0.41~~ or python2.7dist(websocket-client) >= 0.42) with (python2.7dist(websocket-client) < 0.42~~ or python2.7dist(websocket-client) >= 0.43) with python2.7dist(websocket-client) >= 0.32) usr/lib/python2.7/site-packages/zope.component-4.3.0-py2.7.egg-info: provides: |- + purl(pkg:pypi/zope.component@4.3.0) python2.7dist(zope-component) = 4.3 python2dist(zope-component) = 4.3 requires: |- @@ -980,6 +999,7 @@ python2.7dist(zope-interface) >= 4.1 usr/lib/python2.7/site-packages/zope.event-4.2.0.dist-info: provides: |- + purl(pkg:pypi/zope.event@4.2.0) python2.7dist(zope-event) = 4.2 python2dist(zope-event) = 4.2 requires: |- @@ -987,6 +1007,7 @@ python2.7dist(setuptools) usr/lib/python2.7/site-packages/zope.interface-4.6.0.dist-info: provides: |- + purl(pkg:pypi/zope.interface@4.6.0) python2.7dist(zope-interface) = 4.6 python2dist(zope-interface) = 4.6 requires: |- @@ -994,6 +1015,7 @@ python2.7dist(setuptools) usr/lib/python2.7/site-packages/zope.schema-4.4.2-py2.7.egg-info: provides: |- + purl(pkg:pypi/zope.schema@4.4.2) python2.7dist(zope-schema) = 4.4.2 python2dist(zope-schema) = 4.4.2 requires: |- @@ -1003,6 +1025,7 @@ python2.7dist(zope-interface) >= 3.6 usr/lib/python3.9/site-packages/zope.component-4.3.0-py3.9.egg-info: provides: |- + purl(pkg:pypi/zope.component@4.3.0) python3.9dist(zope-component) = 4.3 python3dist(zope-component) = 4.3 requires: |- @@ -1012,6 +1035,7 @@ python3.9dist(zope-interface) >= 4.1 usr/lib/python3.9/site-packages/zope.event-4.2.0.dist-info: provides: |- + purl(pkg:pypi/zope.event@4.2.0) python3.9dist(zope-event) = 4.2 python3dist(zope-event) = 4.2 requires: |- @@ -1019,6 +1043,7 @@ python3.9dist(setuptools) usr/lib/python3.9/site-packages/zope.interface-5.1.0-py3.9.egg-info: provides: |- + purl(pkg:pypi/zope.interface@5.1.0) python3.9dist(zope-interface) = 5.1 python3dist(zope-interface) = 5.1 requires: |- @@ -1026,6 +1051,7 @@ python3.9dist(setuptools) usr/lib/python3.9/site-packages/zope.schema-4.4.2-py3.9.egg-info: provides: |- + purl(pkg:pypi/zope.schema@4.4.2) python3.9dist(zope-schema) = 4.4.2 python3dist(zope-schema) = 4.4.2 requires: |- @@ -1035,11 +1061,13 @@ python3.9dist(zope-interface) >= 3.6 usr/lib64/python2.7/site-packages/backports.range-3.7.2-py2.7.egg-info: provides: |- + purl(pkg:pypi/backports.range@3.7.2) python2.7dist(backports-range) = 3.7.2 python2dist(backports-range) = 3.7.2 requires: python(abi) = 2.7 usr/lib64/python3.7/site-packages/backports.range-3.7.2-py3.7.egg-info: provides: |- + purl(pkg:pypi/backports.range@3.7.2) python3.7dist(backports-range) = 3.7.2 python3dist(backports-range) = 3.7.2 requires: python(abi) = 3.7 @@ -1182,7 +1210,7 @@ python(abi) = 3.9 python3.9dist(certifi) = 2016.9.26 --requires --normalized-names-format pep503 --package-name python3-zope-component+testing: - --provides --majorver-provides --normalized-names-format pep503 --package-name python3-zope-component+testing: + --provides --majorver-provides --normalized-names-format pep503 --purl-provides --package-name python3-zope-component+testing: usr/lib/python3.9/site-packages/zope.component-4.3.0-py3.9.egg-info: provides: |- python3.9dist(zope-component[testing]) = 4.3 diff --git a/tests/pythondist.sh b/tests/pythondist.sh index 51f5511..3442afe 100755 --- a/tests/pythondist.sh +++ b/tests/pythondist.sh @@ -12,6 +12,7 @@ rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^ rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope\.component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-component)' +rpm -qp --provides ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^purl(pkg:pypi/zope\.component@4\.3\.0)$' rpm -qp --requires ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-event)' rpm -qp --requires ${RPMDIR}/python3-zope-component-4.3.0-0.noarch.rpm | grep '^python'$X_Y'dist(zope-interface)' @@ -21,6 +22,7 @@ rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope\.component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-component)' +rpm -qp --provides ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^purl(pkg:pypi/zope\.component@4\.3\.0)$' rpm -qp --requires ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-event)' rpm -qp --requires ${RPMDIR}/python3.7-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.7dist(zope-interface)' @@ -30,6 +32,7 @@ rpm -qp --provides ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep rpm -qp --provides ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.9dist(zope\.component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.9dist(zope-component)' +rpm -qp --provides ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^purl(pkg:pypi/zope\.component@4\.3\.0)$' rpm -qp --requires ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.9dist(zope-event)' rpm -qp --requires ${RPMDIR}/python3.9-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.9dist(zope-interface)' @@ -40,6 +43,7 @@ rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3dist(zope-component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope\.component)' && exit 1 || true rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-component)' +rpm -qp --provides ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^purl(pkg:pypi/zope\.component@4\.3\.0)$' rpm -qp --requires ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-event)' rpm -qp --requires ${RPMDIR}/python3.10-zope-component-4.3.0-0.noarch.rpm | grep '^python3\.10dist(zope-interface)'