[Rpm-ecosystem] RFC: Range dependencies extension to boolean dependencies
ngompa13 at gmail.com
Sun Feb 19 16:19:47 UTC 2017
It comes up with increasing regularity that we don't have a way to
represent ranges of versions for a single dependency (c.f. RHBZ and
RPM GitHub). Most language-specific dependency managers (e.g. pip
for Python, gem for Ruby, composer for PHP, cargo for Rust, npm for
Nodejs, whatever Golang people use, etc.) have some variation of
ranged dependency processing. These ranged dependencies express a
single solvable unit in the form of something like "1.0 < foo <= 1.5"
or something similar.
In RPM, we can currently somewhat represent this relationship in two ways:
1. Using a boolean dependency.
Requires: (foo > 1.0 and foo <= 1.5)
2. Using (Build)Requires + (Build)Conflicts
Requires: foo > 1.0
Conflicts: foo > 1.5
However, there are weaknesses to each approach that make this non-functional.
Consider the boolean dependency form. Suppose an install transaction
processes two boolean deps, one representing "1.0 < foo <= 1.5" and
one representing "2.0 < foo <= 2.5". In the case where nothing is
installed, this works as expected and installs "foo" and "bar".
However, in a situation where "foo" is already installed, "bar" never
gets installed because the condition is satisifed. This leads to a
broken result (as exemplified in this GitHub bug). This is because
each of the components of a boolean dependency is treated as a
separate solvable condition (that is, instead of treating as something
to define a single dependency, it could evaluate to multiple). That
means that it's also possible to trigger the installation of multiple
packages when it isn't desired.
Now consider the Requires + Conflicts form. Suppose a transaction
processes the same two ranges in the form of Requires + Conflicts. The
result leads to an invalid transaction, as the second range is
completely invalidated by the first. This is especially problematic
for long chains of builddeps in ecosystems like Rust and Golang, where
this could fundamentally stop something from getting built. Ecosystems
like Nodejs would be completely broken for runtime installations by
this type of dependency description.
As it turns out, libsolv does support a dependency operand to fix
this: `REL_WITH` (expressed as `+` in libsolv testcases). This binds
two clauses together as a single dependency expression for the
resolver. So, a potential solution for properly express this in rpm
would be to add a `with` statement that provides this logic for
libsolv and dependency resolvers on top of it (DNF and Zypper) to use.
So, for example, "1.0 < foo <= 1.5" and "2.0 < foo <= 2.5" would be
Requires: ((foo > 1.0 with foo <= 1.5) and (foo > 2.0 with foo <= 2.5))
The expression above indicates that two packages will be installed,
each satisfying one of the clauses. "foo" will get installed from the
first clause, and "bar" will get installed from the second clause.
This implements the range concept using the "with" operator, ensuring
that the expression is interpreted to mean one and exactly one
dependency. This grants us the ability to properly handle semantically
versioned dependencies that are common in many libraries now without
the side-effects mentioned earlier.
真実はいつも一つ！/ Always, there's only one truth!
More information about the Rpm-ecosystem