py2rpm: New workaround for differences in PIP and RPM version handling

RPM and pip compare versions differently, and this used to lead to lots
problems, pain and sorrows. The most visible outcome of the difference is
that from RPM's point of view version '2' != '2.0', as well as '2.0' !=
'2.0.0', but for pip it's same version.

This change implements new workaround for this. It works as follows: if
python module requires module of some version, (like 2.0.0), the actual RPM
version of the module will have the same non-zero beginning and some '.0's at
the end ('2', '2.0', '2.0.0', '2.0.0.0' ...). Thus, we can calculate lower
bound for requirement by trimming '.0' from the version (we get '2'), and
then set upper bound to lower bound plus tail of '.0' repeated several times.
Luckily, '2.0' and '2.00' is the same version for RPM.

Then, exact requirements (like 'pysendfile==2.0.0') are replaced with
range (pysendfile >= 2, pysendfile <= 2.0.0.0.0.0.0.0.0.0.0.0), while
for '<=' and '>=' we use only upper or lower bound correspondingly.

Also, we do not trim zeroes from versions that we put in Version: tag, as we
don't need to do that any more.

Closes-bug: #1254991
Change-Id: Idb142d0f85d60183e79964baece52e0128cf9d7b
This commit is contained in:
Ivan A. Melnikov 2013-11-27 14:02:28 +04:00
parent bafa115cc7
commit ba154fa614

View File

@ -373,16 +373,19 @@ exec(compile(open(__file__).read().replace('\\r\\n', '\\n'), __file__, 'exec'))
def trim_zeroes(version): def trim_zeroes(version):
"""RPM mishandles versions like "0.8.0". Make it happy.""" """Trim zeroes from the and of a version"""
match = version_re.match(version) match = version_re.match(version)
if match: if match:
return match.group(1) return match.group(1)
return version return version
_DOT_ZEROES_TAIL = '.0' * 10
def requires_and_conflicts(req_list, skip_req_names=()): def requires_and_conflicts(req_list, skip_req_names=()):
rpm_requires = "" rpm_requires = []
rpm_conflicts = "" rpm_conflicts = []
for line in req_list: for line in req_list:
try: try:
req = pkg_resources.Requirement.parse(line) req = pkg_resources.Requirement.parse(line)
@ -392,28 +395,50 @@ def requires_and_conflicts(req_list, skip_req_names=()):
continue continue
rpm_name = python_key_to_rpm(req.key) rpm_name = python_key_to_rpm(req.key)
if not req.specs: if not req.specs:
rpm_requires += "\nRequires:" rpm_requires.append(rpm_name)
rpm_requires = "%s %s" % ( for kind, version in req.specs:
rpm_requires, rpm_name)
for spec in req.specs:
# kind in ("==", "<=", ">=", "!=")
kind = spec[0]
version = trim_zeroes(spec[1])
try: try:
version = "%s:%s" % (epoch_map[req.key], version) version = "%s:%s" % (epoch_map[req.key], version)
except KeyError: except KeyError:
pass pass
# NOTE(imelnikov): rpm and pip compare versions differently, and
# this used to lead to lots problems, pain and sorrows. The most
# visible outcome of the difference is that from rpm's point of
# view version '2' != '2.0', as well as '2.0' != '2.0.0', but for
# pip it's same version.
#
# Current workaround for this works as follows: if python module
# requires module of some version, (like 2.0.0), the actual rpm
# version of the module will have the same non-zero beginning and
# some '.0's at the end ('2', '2.0', '2.0.0', '2.0.0.0' ...). Thus,
# we can calculate lower bound for requirement by trimming '.0'
# from the version (we get '2'), and then set upper bound to lower
# bound + tail of '.0' repeated several times. Luckily, '2.0' and
# '2.00' is the same version for rpm.
lower_version = trim_zeroes(version)
upper_version = '%s%s' % (lower_version, _DOT_ZEROES_TAIL)
if kind == "!=": if kind == "!=":
rpm_conflicts += "\nConflicts:" # NOTE(imelnikov): we can't conflict with ranges, so we
rpm_conflicts = "%s %s = %s" % ( # put version as is and with trimmed zeroes just in case
rpm_conflicts, rpm_name, version) rpm_conflicts.append('%s == %s' % (rpm_name, version))
continue if version != lower_version:
if kind == "==": rpm_conflicts.append('%s == %s' % (rpm_name, lower_version))
kind = "=" elif kind == '==':
rpm_requires += "\nRequires:" rpm_requires.extend((
rpm_requires = "%s %s %s %s" % ( '%s >= %s' % (rpm_name, lower_version),
rpm_requires, rpm_name, kind, version) '%s <= %s' % (rpm_name, upper_version)
return rpm_requires, rpm_conflicts ))
elif kind in ('<=', '<'):
rpm_requires.append('%s <= %s' % (rpm_name, upper_version))
elif kind in ('>=', '>'):
rpm_requires.append('%s >= %s' % (rpm_name, lower_version))
else:
raise ValueError('Invalid requirement kind: %r' % kind)
rpm_requires_str = ''.join("\nRequires: %s" % req for req in rpm_requires)
rpm_conflicts_str = ''.join("\nConflicts: %s" % req for req in rpm_conflicts)
return rpm_requires_str, rpm_conflicts_str
def one_line(line, max_len=80): def one_line(line, max_len=80):
@ -465,11 +490,7 @@ def build_rpm(options, filename):
archive_name = "%s/dist/%s-%s.tar.gz" % (source_dir, pkg_name, version) archive_name = "%s/dist/%s-%s.tar.gz" % (source_dir, pkg_name, version)
shutil.copy(archive_name, os.path.join(build_dir, "SOURCES")) shutil.copy(archive_name, os.path.join(build_dir, "SOURCES"))
# We need to do this so that when a package such as hacking depends on cleaned_version = version.replace('-', '_')
# flake8 v2 that we don't go ahead and build a v2.0 version.
#
# Note(harlowja): Not sure why rpm seems to not understand these are the same...
cleaned_version = trim_zeroes(version.replace('-', '_'))
with open(spec_name, "w") as spec_file: with open(spec_name, "w") as spec_file:
print >> spec_file, "%define pkg_name", pkg_name print >> spec_file, "%define pkg_name", pkg_name
print >> spec_file, "%define pkg_path", os.path.join(*pkg_name.split('.')) print >> spec_file, "%define pkg_path", os.path.join(*pkg_name.split('.'))