aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKaren Arutyunov <karen@codesynthesis.com>2023-01-19 16:26:21 +0300
committerKaren Arutyunov <karen@codesynthesis.com>2023-01-19 16:28:58 +0300
commit098b7407f8ca04a0e060b0692ae028e680292f87 (patch)
treeb7a8ed6a90652efc8e479ece5be170cb83d53d4b
parentf42795b9a9b435f9db94922eba47e44fa18f56e5 (diff)
Implement system_package_manager::downstream_package_version()
-rw-r--r--bpkg/system-package-manager.cxx346
1 files changed, 288 insertions, 58 deletions
diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx
index 78c5ce3..5e933ae 100644
--- a/bpkg/system-package-manager.cxx
+++ b/bpkg/system-package-manager.cxx
@@ -3,6 +3,9 @@
#include <bpkg/system-package-manager.hxx>
+#include <sstream>
+
+#include <libbutl/regex.hxx>
#include <libbutl/semantic-version.hxx>
#include <bpkg/package.hxx>
@@ -77,29 +80,97 @@ namespace bpkg
return r;
}
- strings system_package_manager::
- system_package_names (const available_packages& aps,
- const string& name_id,
- const string& version_id,
- const vector<string>& like_ids)
+ // Return the version id parsed as a semantic version if it is not empty and
+ // the "0" semantic version otherwise. Issue diagnostics and fail on the
+ // parsing error.
+ //
+ // Note: the name_id argument is only used for diagnostics.
+ //
+ static inline semantic_version
+ parse_version_id (const string& version_id, const string& name_id)
{
- assert (!aps.empty ());
+ if (version_id.empty ())
+ return semantic_version (0, 0, 0);
+
+ try
+ {
+ return semantic_version (version_id, semantic_version::allow_omit_minor);
+ }
+ catch (const invalid_argument& e)
+ {
+ fail << "invalid version '" << version_id << "' for " << name_id
+ << " operating system: " << e << endf;
+ }
+ }
+
+ // Parse the <distribution> component of the specified <distribution>-*
+ // value into the distribution name and version (returne as "0" if not
+ // present). Issue diagnostics and fail on the parsing error.
+ //
+ // Note: the value_name, ap, and af arguments are only used for diagnostics.
+ //
+ static pair<string, semantic_version>
+ parse_distribution (string&& d,
+ const string& value_name,
+ const shared_ptr<available_package>& ap,
+ const lazy_shared_ptr<repository_fragment>& af)
+ {
+ string dn (move (d)); // <name>[_<version>]
+ size_t p (dn.rfind ('_')); // Version-separating underscore.
- // Parse the version id if it is not empty and leave it "0" otherwise.
+ // If '_' separator is present, then make sure that the right-hand part
+ // looks like a version (is not empty and only contains digits and dots).
//
- semantic_version vid;
+ if (p != string::npos)
+ {
+ if (p != dn.size () - 1)
+ {
+ for (size_t i (p + 1); i != dn.size (); ++i)
+ {
+ if (!digit (dn[i]) && dn[i] != '.')
+ {
+ p = string::npos;
+ break;
+ }
+ }
+ }
+ else
+ p = string::npos;
+ }
- if (!version_id.empty ())
+ // Parse the distribution version if present and leave it "0" otherwise.
+ //
+ semantic_version dv (0, 0, 0);
+ if (p != string::npos)
try
{
- vid = semantic_version (version_id, semantic_version::allow_omit_minor);
+ dv = semantic_version (dn,
+ p + 1,
+ semantic_version::allow_omit_minor);
+
+ dn.resize (p);
}
catch (const invalid_argument& e)
{
- fail << "invalid version '" << version_id << "' for " << name_id
- << " operating system: " << e;
+ fail << "invalid distribution version '" << string (dn, p + 1)
+ << "' in value " << value_name << " for package " << ap->id.name
+ << ' ' << ap->version << af.database () << " in repository "
+ << af.load ()->location << ": " << e;
}
+ return make_pair (move (dn), move (dv));
+ }
+
+ strings system_package_manager::
+ system_package_names (const available_packages& aps,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids)
+ {
+ assert (!aps.empty ());
+
+ semantic_version vid (parse_version_id (version_id, name_id));
+
// Return those <name>[_<version>]-name distribution values of the
// specified available packages whose <name> component matches the
// specified distribution name and the <version> component (assumed as "0"
@@ -120,53 +191,11 @@ namespace bpkg
{
if (optional<string> d = nv.distribution ("-name"))
{
- string dn (move (*d)); // <name>[_<version>]
- size_t p (dn.rfind ('_')); // Version-separating underscore.
+ pair<string, semantic_version> dnv (
+ parse_distribution (move (*d), nv.name, ap, a.second));
- // If '_' separator is present, then make sure that the right-hand
- // part looks like a version (not empty and only contains digits
- // and dots).
- //
- if (p != string::npos)
- {
- if (p != dn.size () - 1)
- {
- for (size_t i (p); i != dn.size (); ++i)
- {
- if (!digit (dn[i]) && dn[i] != '.')
- {
- p = string::npos;
- break;
- }
- }
- }
- else
- p = string::npos;
- }
-
- // Parse the distribution version if present and leave it "0"
- // otherwise.
- //
- semantic_version dv;
- if (p != string::npos)
- try
- {
- dv = semantic_version (dn,
- p + 1,
- semantic_version::allow_omit_minor);
- }
- catch (const invalid_argument& e)
- {
- fail << "invalid distribution version in value " << nv.name
- << " for package " << ap->id.name << ' ' << ap->version
- << a.second.database () << " in repository "
- << a.second.load ()->location << ": " << e;
- }
-
- dn.resize (p);
-
- if (dn == n &&
- dv <= v &&
+ if (dnv.first == n &&
+ dnv.second <= v &&
find_if (r.begin (), r.end (),
[&nv] (const distribution_name_value& v)
{return v.name == nv.name || v.value == nv.value;}) ==
@@ -213,4 +242,205 @@ namespace bpkg
return r;
}
+
+ optional<version> system_package_manager::
+ downstream_package_version (const string& system_version,
+ const available_packages& aps,
+ const string& name_id,
+ const string& version_id,
+ const vector<string>& like_ids)
+ {
+ semantic_version vid (parse_version_id (version_id, name_id));
+
+ // Iterate over the passed available packages (in version descending
+ // order) and over the <name>[_<version>]-to-downstream-version
+ // distribution values they contain. Only consider those values whose
+ // <name> component matches the specified distribution name and the
+ // <version> component (assumed as "0" if not present) is less or equal
+ // the specified distribution version. For such values match the regex
+ // pattern against the passed system version and if it matches consider
+ // the replacement as the resulting downstream version candidate. Return
+ // this downstream version if the distribution version is equal to the
+ // specified one. Otherwise (the version is less), continue iterating
+ // while preferring downstream version candidates for greater distribution
+ // versions. Note that here we are trying to use a version mapping for the
+ // distribution version most close (but never greater) to the specified
+ // distribution version. So, for example, if both following values contain
+ // a matching mapping, then for debian 11 we prefer the downstream version
+ // produced by the debian_10-to-downstream-version value:
+ //
+ // debian_9-to-downstream-version
+ // debian_10-to-downstream-version
+ //
+ auto downstream_version = [&aps, &system_version]
+ (const string& n, const semantic_version& v)
+ -> optional<version>
+ {
+ optional<version> r;
+ semantic_version rv;
+
+ for (const auto& a: aps)
+ {
+ const shared_ptr<available_package>& ap (a.first);
+
+ for (const distribution_name_value& nv: ap->distribution_values)
+ {
+ if (optional<string> d = nv.distribution ("-to-downstream-version"))
+ {
+ pair<string, semantic_version> dnv (
+ parse_distribution (move (*d), nv.name, ap, a.second));
+
+ if (dnv.first == n && dnv.second <= v)
+ {
+ auto bad_value = [&nv, &ap, &a] (const string& d)
+ {
+ const lazy_shared_ptr<repository_fragment>& af (a.second);
+
+ fail << "invalid distribution value '" << nv.name << ": "
+ << nv.value << "' for package " << ap->id.name << ' '
+ << ap->version << af.database () << " in repository "
+ << af.load ()->location << ": " << d;
+ };
+
+ // Parse the distribution value into the regex pattern and the
+ // replacement.
+ //
+ const string& val (nv.value);
+
+ if (val[0] != '/')
+ bad_value ("first character must be '/'");
+
+ if (val.size () < 2 || val.back () != '/')
+ bad_value ("last character must be '/'");
+
+ // Find '/' which separates the pattern and replacement
+ // components. For good measure, search backwards from position
+ // which preceeds the trailing '/'.
+ //
+ size_t p (val.rfind ('/', val.size () - 2));
+ assert (p != string::npos); // There is always the leading '/'.
+
+ if (p == 0)
+ bad_value ("no replacement component");
+
+ string pat (val, 1, p - 1);
+ string rep (val, p + 1, val.size () - p - 2);
+
+ // Match the regex and skip it if it doesn't match or proceed to
+ // parsing the downstream version resulting from the regex
+ // replacement otherwise.
+ //
+ string dv;
+ try
+ {
+ regex re (pat, regex::ECMAScript);
+
+ pair<string, bool> rr (
+ regex_replace_match (system_version, re, rep));
+
+ // Skip the regex if it doesn't match.
+ //
+ if (!rr.second)
+ continue;
+
+ dv = move (rr.first);
+ }
+ catch (const regex_error& e)
+ {
+ // Print regex_error description if meaningful (no space).
+ //
+ ostringstream os;
+ os << "invalid regex pattern '" << pat << "'" << e;
+ bad_value (os.str ());
+ }
+
+ // Parse the downstream version.
+ //
+ try
+ {
+ version ver (dv);
+
+ // If the distribution version is equal to the specified one,
+ // then we are done. Otherwise, save the version if it is
+ // preferable and continue iterating.
+ //
+ // Note that bailing out immediately in the former case is
+ // essential. Otherwise, we can potentially fail later on, for
+ // example, some ill-formed regex which is already fixed in
+ // some newer package.
+ //
+ if (dnv.second == v)
+ return ver;
+
+ if (!r || rv < dnv.second)
+ {
+ r = move (ver);
+ rv = move (dnv.second);
+ }
+ }
+ catch (const invalid_argument& e)
+ {
+ bad_value ("resulting downstream version '" + dv +
+ "' is invalid: " + e.what ());
+ }
+ }
+ }
+ //
+ // Try to also deduce the downstream version using the *-version
+ // distribution value.
+ //
+ else if (!ap->stub ())
+ {
+ // Note that the below logic is a simplified version of the above
+ // *-to-downstream-version value handling.
+ //
+ if (optional<string> d = nv.distribution ("-version"))
+ {
+ pair<string, semantic_version> dnv (
+ parse_distribution (move (*d), nv.name, ap, a.second));
+
+ if (dnv.first == n && dnv.second <= v)
+ {
+ if (nv.value == system_version)
+ {
+ if (dnv.second == v)
+ return ap->version;
+
+ if (!r || rv < dnv.second)
+ {
+ r = ap->version;
+ rv = move (dnv.second);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return r;
+ };
+
+ // Try to deduce the downstream version using the
+ // <distribution>-to-downstream-version values that match the name id and
+ // refer to the version which is less or equal than the version id.
+ //
+ optional<version> r (downstream_version (name_id, vid));
+
+ // If the downstream version is not deduced and the like ids are
+ // specified, then re-try but now using the like id and "0" version id
+ // instead.
+ //
+ if (!r)
+ {
+ for (const string& like_id: like_ids)
+ {
+ r = downstream_version (like_id, semantic_version (0, 0, 0));
+ if (r)
+ break;
+ }
+ }
+
+ return r;
+ }
}