%pyproject_buildrequires now fails when it encounters an invalid requirement
Fixes https://bugzilla.redhat.com/show_bug.cgi?id=1983053
This commit is contained in:
parent
3406e9332b
commit
f8a3343abc
34
README.md
34
README.md
@ -268,8 +268,42 @@ undefine `%{py3_shbang_opt}` to turn it off.
|
|||||||
|
|
||||||
Some valid Python version specifiers are not supported.
|
Some valid Python version specifiers are not supported.
|
||||||
|
|
||||||
|
When a dependency is specified via an URL or local path, for example as:
|
||||||
|
|
||||||
|
https://github.com/ActiveState/appdirs/archive/8eacfa312d77aba28d483fbfb6f6fc54099622be.zip
|
||||||
|
/some/path/foo-1.2.3.tar.gz
|
||||||
|
git+https://github.com/sphinx-doc/sphinx.git@96dbe5e3
|
||||||
|
|
||||||
|
The `%pyproject_buildrequires` macro is unable to convert it to an appropriate RPM requirement and will fail.
|
||||||
|
If the URL contains the `packageName @` prefix as specified in [PEP 508],
|
||||||
|
the requirement will be generated without a version constraint:
|
||||||
|
|
||||||
|
appdirs@https://github.com/ActiveState/appdirs/archive/8eacfa312d77aba28d483fbfb6f6fc54099622be.zip
|
||||||
|
foo@file:///some/path/foo-1.2.3.tar.gz
|
||||||
|
|
||||||
|
Will be converted to:
|
||||||
|
|
||||||
|
python3dist(appdirs)
|
||||||
|
python3dist(foo)
|
||||||
|
|
||||||
|
Alternatively, when an URL requirement parsed from a text file
|
||||||
|
given as positional argument to `%pyproject_buildrequires`
|
||||||
|
contains the `#egg=packageName` fragment,
|
||||||
|
as documented in [pip's documentation]:
|
||||||
|
|
||||||
|
git+https://github.com/sphinx-doc/sphinx.git@96dbe5e3#egg=sphinx
|
||||||
|
|
||||||
|
The requirements will be converted to package names without versions, e.g.:
|
||||||
|
|
||||||
|
python3dist(sphinx)
|
||||||
|
|
||||||
|
However upstreams usually only use direct URLs for their requirements as workarounds,
|
||||||
|
so be prepared for problems.
|
||||||
|
|
||||||
|
[PEP 508]: https://www.python.org/dev/peps/pep-0508/
|
||||||
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
[PEP 517]: https://www.python.org/dev/peps/pep-0517/
|
||||||
[PEP 518]: https://www.python.org/dev/peps/pep-0518/
|
[PEP 518]: https://www.python.org/dev/peps/pep-0518/
|
||||||
|
[pip's documentation]: https://pip.pypa.io/en/stable/cli/pip_install/#vcs-support
|
||||||
|
|
||||||
|
|
||||||
Testing the macros
|
Testing the macros
|
||||||
|
@ -6,7 +6,7 @@ License: MIT
|
|||||||
|
|
||||||
# Keep the version at zero and increment only release
|
# Keep the version at zero and increment only release
|
||||||
Version: 0
|
Version: 0
|
||||||
Release: 45%{?dist}
|
Release: 46%{?dist}
|
||||||
|
|
||||||
# Macro files
|
# Macro files
|
||||||
Source001: macros.pyproject
|
Source001: macros.pyproject
|
||||||
@ -109,6 +109,10 @@ export HOSTNAME="rpmbuild" # to speedup tox in network-less mock, see rhbz#1856
|
|||||||
%license LICENSE
|
%license LICENSE
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Fri Jul 23 2021 Miro Hrončok <miro@hroncok.cz> - 0-46
|
||||||
|
- %%pyproject_buildrequires now fails when it encounters an invalid requirement
|
||||||
|
- Fixes: rhbz#1983053
|
||||||
|
|
||||||
* Fri Jul 23 2021 Fedora Release Engineering <releng@fedoraproject.org> - 0-45
|
* Fri Jul 23 2021 Fedora Release Engineering <releng@fedoraproject.org> - 0-45
|
||||||
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
|
- Rebuilt for https://fedoraproject.org/wiki/Fedora_35_Mass_Rebuild
|
||||||
|
|
||||||
|
@ -11,12 +11,16 @@ import re
|
|||||||
import tempfile
|
import tempfile
|
||||||
import email.parser
|
import email.parser
|
||||||
import pathlib
|
import pathlib
|
||||||
|
import urllib
|
||||||
|
|
||||||
|
|
||||||
# Some valid Python version specifiers are not supported.
|
# Some valid Python version specifiers are not supported.
|
||||||
# Allow only the forms we know we can handle.
|
# Allow only the forms we know we can handle.
|
||||||
VERSION_RE = re.compile(r'[a-zA-Z0-9.-]+(\.\*)?')
|
VERSION_RE = re.compile(r'[a-zA-Z0-9.-]+(\.\*)?')
|
||||||
|
|
||||||
|
# We treat this as comment in requirements files, as does pip
|
||||||
|
COMMENT_RE = re.compile(r'(^|\s+)#.*$')
|
||||||
|
|
||||||
|
|
||||||
class EndPass(Exception):
|
class EndPass(Exception):
|
||||||
"""End current pass of generating requirements"""
|
"""End current pass of generating requirements"""
|
||||||
@ -50,6 +54,31 @@ def hook_call():
|
|||||||
print_err('HOOK STDOUT:', line)
|
print_err('HOOK STDOUT:', line)
|
||||||
|
|
||||||
|
|
||||||
|
def pkgname_from_egg_fragment(requirement_str):
|
||||||
|
parsed_url = urllib.parse.urlparse(requirement_str)
|
||||||
|
parsed_fragment = urllib.parse.parse_qs(parsed_url.fragment)
|
||||||
|
if 'egg' in parsed_fragment:
|
||||||
|
return parsed_fragment['egg'][0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def guess_reason_for_invalid_requirement(requirement_str):
|
||||||
|
if ':' in requirement_str:
|
||||||
|
return (
|
||||||
|
'It might be an URL. '
|
||||||
|
'%pyproject_buildrequires cannot handle all URL-based requirements. '
|
||||||
|
'Add PackageName@ (see PEP 508) to the URL to at least require any version of PackageName.'
|
||||||
|
)
|
||||||
|
if '/' in requirement_str:
|
||||||
|
return (
|
||||||
|
'It might be a local path. '
|
||||||
|
'%pyproject_buildrequires cannot handle local paths as requirements. '
|
||||||
|
'Use an URL with PackageName@ (see PEP 508) to at least require any version of PackageName.'
|
||||||
|
)
|
||||||
|
# No more ideas
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class Requirements:
|
class Requirements:
|
||||||
"""Requirement printer"""
|
"""Requirement printer"""
|
||||||
def __init__(self, get_installed_version, extras=None,
|
def __init__(self, get_installed_version, extras=None,
|
||||||
@ -81,18 +110,27 @@ class Requirements:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def add(self, requirement_str, *, source=None):
|
def add(self, requirement_str, *, source=None, allow_egg_pkgname=False):
|
||||||
"""Output a Python-style requirement string as RPM dep"""
|
"""Output a Python-style requirement string as RPM dep"""
|
||||||
print_err(f'Handling {requirement_str} from {source}')
|
print_err(f'Handling {requirement_str} from {source}')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
requirement = Requirement(requirement_str)
|
requirement = Requirement(requirement_str)
|
||||||
except InvalidRequirement as e:
|
except InvalidRequirement:
|
||||||
|
if allow_egg_pkgname and (egg_name := pkgname_from_egg_fragment(requirement_str)):
|
||||||
|
requirement = Requirement(egg_name)
|
||||||
|
requirement.url = requirement_str
|
||||||
|
else:
|
||||||
|
hint = guess_reason_for_invalid_requirement(requirement_str)
|
||||||
|
message = f'Requirement {requirement_str!r} from {source} is invalid.'
|
||||||
|
if hint:
|
||||||
|
message += f' Hint: {hint}'
|
||||||
|
raise ValueError(message)
|
||||||
|
|
||||||
|
if requirement.url:
|
||||||
print_err(
|
print_err(
|
||||||
f'WARNING: Skipping invalid requirement: {requirement_str}\n'
|
f'WARNING: Simplifying {requirement_str!r} to {requirement.name!r}.'
|
||||||
+ f' {e}',
|
|
||||||
)
|
)
|
||||||
return
|
|
||||||
|
|
||||||
name = canonicalize_name(requirement.name)
|
name = canonicalize_name(requirement.name)
|
||||||
if (requirement.marker is not None and
|
if (requirement.marker is not None and
|
||||||
@ -128,7 +166,7 @@ class Requirements:
|
|||||||
if not VERSION_RE.fullmatch(str(specifier.version)):
|
if not VERSION_RE.fullmatch(str(specifier.version)):
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f'Unknown character in version: {specifier.version}. '
|
f'Unknown character in version: {specifier.version}. '
|
||||||
+ '(This is probably a bug in pyproject-rpm-macros.)',
|
+ '(This might be a bug in pyproject-rpm-macros.)',
|
||||||
)
|
)
|
||||||
together.append(convert(python3dist(name, python3_pkgversion=self.python3_pkgversion),
|
together.append(convert(python3dist(name, python3_pkgversion=self.python3_pkgversion),
|
||||||
specifier.operator, specifier.version))
|
specifier.operator, specifier.version))
|
||||||
@ -146,10 +184,10 @@ class Requirements:
|
|||||||
print_err(f'Exiting dependency generation pass: {source}')
|
print_err(f'Exiting dependency generation pass: {source}')
|
||||||
raise EndPass(source)
|
raise EndPass(source)
|
||||||
|
|
||||||
def extend(self, requirement_strs, *, source=None):
|
def extend(self, requirement_strs, **kwargs):
|
||||||
"""add() several requirements"""
|
"""add() several requirements"""
|
||||||
for req_str in requirement_strs:
|
for req_str in requirement_strs:
|
||||||
self.add(req_str, source=source)
|
self.add(req_str, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
def get_backend(requirements):
|
def get_backend(requirements):
|
||||||
@ -241,13 +279,7 @@ def generate_run_requirements(backend, requirements):
|
|||||||
def parse_requirements_lines(lines, path=None):
|
def parse_requirements_lines(lines, path=None):
|
||||||
packages = []
|
packages = []
|
||||||
for line in lines:
|
for line in lines:
|
||||||
line, _, comment = line.partition('#')
|
line = COMMENT_RE.sub('', line)
|
||||||
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('&')
|
|
||||||
line = egg[4:]
|
|
||||||
line = line.strip()
|
line = line.strip()
|
||||||
if line.startswith('-r'):
|
if line.startswith('-r'):
|
||||||
recursed_path = line[2:].strip()
|
recursed_path = line[2:].strip()
|
||||||
@ -343,7 +375,8 @@ def generate_requires(
|
|||||||
lines = req_file.read().splitlines()
|
lines = req_file.read().splitlines()
|
||||||
packages = parse_requirements_lines(lines, pathlib.Path(req_file.name))
|
packages = parse_requirements_lines(lines, pathlib.Path(req_file.name))
|
||||||
requirements.extend(packages,
|
requirements.extend(packages,
|
||||||
source=f'requirements file {req_file.name}')
|
source=f'requirements file {req_file.name}',
|
||||||
|
allow_egg_pkgname=True)
|
||||||
requirements.check(source='all requirement files')
|
requirements.check(source='all requirement files')
|
||||||
if use_build_system:
|
if use_build_system:
|
||||||
backend = get_backend(requirements)
|
backend = get_backend(requirements)
|
||||||
|
@ -92,8 +92,7 @@ Single value version with unsupported compatible operator:
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["pkg ~= 42", "foo"]
|
requires = ["pkg ~= 42", "foo"]
|
||||||
build-backend = "foo.build"
|
build-backend = "foo.build"
|
||||||
stderr_contains: "WARNING: Skipping invalid requirement: pkg ~= 42"
|
except: ValueError
|
||||||
result: 0
|
|
||||||
|
|
||||||
Asterisk in version with unsupported compatible operator:
|
Asterisk in version with unsupported compatible operator:
|
||||||
installed:
|
installed:
|
||||||
@ -102,8 +101,34 @@ Asterisk in version with unsupported compatible operator:
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["pkg ~= 0.1.*", "foo"]
|
requires = ["pkg ~= 0.1.*", "foo"]
|
||||||
build-backend = "foo.build"
|
build-backend = "foo.build"
|
||||||
stderr_contains: "WARNING: Skipping invalid requirement: pkg ~= 0.1.*"
|
except: ValueError
|
||||||
result: 0
|
|
||||||
|
Local path as requirement:
|
||||||
|
installed:
|
||||||
|
toml: 1
|
||||||
|
pyproject.toml: |
|
||||||
|
[build-system]
|
||||||
|
requires = ["./pkg-1.2.3.tar.gz", "foo"]
|
||||||
|
build-backend = "foo.build"
|
||||||
|
except: ValueError
|
||||||
|
|
||||||
|
Pip's egg=pkgName requirement not in requirements file:
|
||||||
|
installed:
|
||||||
|
toml: 1
|
||||||
|
pyproject.toml: |
|
||||||
|
[build-system]
|
||||||
|
requires = ["git+https://github.com/monty/spam.git@master#egg=spam", "foo"]
|
||||||
|
build-backend = "foo.build"
|
||||||
|
except: ValueError
|
||||||
|
|
||||||
|
URL without egg fragment as requirement:
|
||||||
|
installed:
|
||||||
|
toml: 1
|
||||||
|
pyproject.toml: |
|
||||||
|
[build-system]
|
||||||
|
requires = ["git+https://github.com/pkg-dev/pkg.git@96dbe5e3", "foo"]
|
||||||
|
build-backend = "foo.build"
|
||||||
|
except: ValueError
|
||||||
|
|
||||||
Build system dependencies in pyproject.toml with extras:
|
Build system dependencies in pyproject.toml with extras:
|
||||||
generate_extras: true
|
generate_extras: true
|
||||||
@ -125,9 +150,9 @@ Build system dependencies in pyproject.toml with extras:
|
|||||||
"equal == 0.5.0",
|
"equal == 0.5.0",
|
||||||
"arbitrary_equal === 0.6.0",
|
"arbitrary_equal === 0.6.0",
|
||||||
"asterisk_equal == 0.6.*",
|
"asterisk_equal == 0.6.*",
|
||||||
|
"appdirs@https://github.com/ActiveState/appdirs/archive/8eacfa312d77aba28d483fbfb6f6fc54099622be.zip",
|
||||||
"multi[Extras1,Extras2] == 6.0",
|
"multi[Extras1,Extras2] == 6.0",
|
||||||
"combo >2, <5, != 3.0.0",
|
"combo >2, <5, != 3.0.0",
|
||||||
"invalid!!ignored",
|
|
||||||
"py2 ; python_version < '2.7'",
|
"py2 ; python_version < '2.7'",
|
||||||
"py3 ; python_version > '3.0'",
|
"py3 ; python_version > '3.0'",
|
||||||
]
|
]
|
||||||
@ -145,11 +170,13 @@ Build system dependencies in pyproject.toml with extras:
|
|||||||
python3dist(equal) = 0.5
|
python3dist(equal) = 0.5
|
||||||
python3dist(arbitrary-equal) = 0.6
|
python3dist(arbitrary-equal) = 0.6
|
||||||
(python3dist(asterisk-equal) >= 0.6 with python3dist(asterisk-equal) < 0.7)
|
(python3dist(asterisk-equal) >= 0.6 with python3dist(asterisk-equal) < 0.7)
|
||||||
|
python3dist(appdirs)
|
||||||
python3dist(multi) = 6
|
python3dist(multi) = 6
|
||||||
python3dist(multi[extras1]) = 6
|
python3dist(multi[extras1]) = 6
|
||||||
python3dist(multi[extras2]) = 6
|
python3dist(multi[extras2]) = 6
|
||||||
((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5 with python3dist(combo) > 2)
|
((python3dist(combo) < 3 or python3dist(combo) > 3) with python3dist(combo) < 5 with python3dist(combo) > 2)
|
||||||
python3dist(py3)
|
python3dist(py3)
|
||||||
|
stderr_contains: "WARNING: Simplifying 'appdirs@https://github.com/ActiveState/appdirs/archive/8eacfa312d77aba28d483fbfb6f6fc54099622be.zip' to 'appdirs'."
|
||||||
result: 0
|
result: 0
|
||||||
|
|
||||||
Build system dependencies in pyproject.toml without extras:
|
Build system dependencies in pyproject.toml without extras:
|
||||||
@ -576,6 +603,7 @@ With pyproject.toml, requirements file and with -N option:
|
|||||||
python3dist(paramiko)
|
python3dist(paramiko)
|
||||||
python3dist(sqlalchemy)
|
python3dist(sqlalchemy)
|
||||||
python3dist(spam)
|
python3dist(spam)
|
||||||
|
stderr_contains: "WARNING: Simplifying 'git+https://github.com/monty/spam.git@master#egg=spam' to 'spam'."
|
||||||
result: 0
|
result: 0
|
||||||
|
|
||||||
With pyproject.toml, requirements file and without -N option:
|
With pyproject.toml, requirements file and without -N option:
|
||||||
|
@ -28,6 +28,9 @@ Summary: %{summary}
|
|||||||
# we don't have pip-tools packaged in Fedora yet
|
# we don't have pip-tools packaged in Fedora yet
|
||||||
sed -i /pip-tools/d requirements/dev.in
|
sed -i /pip-tools/d requirements/dev.in
|
||||||
|
|
||||||
|
# help the macros understand the URL in requirements/docs.in
|
||||||
|
sed -Ei 's/sphinx\.git@([0-9a-f]+)/sphinx.git@\1#egg=sphinx/' requirements/docs.in
|
||||||
|
|
||||||
|
|
||||||
%generate_buildrequires
|
%generate_buildrequires
|
||||||
# requirements/dev.in recursively includes tests.in and docs.in
|
# requirements/dev.in recursively includes tests.in and docs.in
|
||||||
|
Loading…
Reference in New Issue
Block a user