From 098b7407f8ca04a0e060b0692ae028e680292f87 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Thu, 19 Jan 2023 16:26:21 +0300 Subject: Implement system_package_manager::downstream_package_version() --- bpkg/system-package-manager.cxx | 346 +++++++++++++++++++++++++++++++++------- 1 file 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 +#include + +#include #include #include @@ -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& 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 component of the specified -* + // 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 + parse_distribution (string&& d, + const string& value_name, + const shared_ptr& ap, + const lazy_shared_ptr& af) + { + string dn (move (d)); // [_] + 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& like_ids) + { + assert (!aps.empty ()); + + semantic_version vid (parse_version_id (version_id, name_id)); + // Return those [_]-name distribution values of the // specified available packages whose component matches the // specified distribution name and the component (assumed as "0" @@ -120,53 +191,11 @@ namespace bpkg { if (optional d = nv.distribution ("-name")) { - string dn (move (*d)); // [_] - size_t p (dn.rfind ('_')); // Version-separating underscore. + pair 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 system_package_manager:: + downstream_package_version (const string& system_version, + const available_packages& aps, + const string& name_id, + const string& version_id, + const vector& 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 [_]-to-downstream-version + // distribution values they contain. Only consider those values whose + // component matches the specified distribution name and the + // 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 + { + optional r; + semantic_version rv; + + for (const auto& a: aps) + { + const shared_ptr& ap (a.first); + + for (const distribution_name_value& nv: ap->distribution_values) + { + if (optional d = nv.distribution ("-to-downstream-version")) + { + pair 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& 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 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 d = nv.distribution ("-version")) + { + pair 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 + // -to-downstream-version values that match the name id and + // refer to the version which is less or equal than the version id. + // + optional 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; + } } -- cgit v1.1