From 8a19b1c66fc67bc16be435d7c3e62cc457afc958 Mon Sep 17 00:00:00 2001 From: Karen Arutyunov Date: Mon, 30 Jan 2023 14:42:15 +0300 Subject: Continue adding Fedora support --- bpkg/system-package-manager-fedora.cxx | 1072 ++++++++++++++++++++++++++++---- bpkg/system-package-manager-fedora.hxx | 95 ++- 2 files changed, 1013 insertions(+), 154 deletions(-) diff --git a/bpkg/system-package-manager-fedora.cxx b/bpkg/system-package-manager-fedora.cxx index a22bfba..e3e7f19 100644 --- a/bpkg/system-package-manager-fedora.cxx +++ b/bpkg/system-package-manager-fedora.cxx @@ -19,7 +19,8 @@ namespace bpkg // extra_{doc,dbg} arguments. // package_status system_package_manager_fedora:: - parse_name_value (const string& nv, + parse_name_value (const package_name& pn, + const string& nv, bool extra_doc, bool extra_debuginfo, bool extra_debugsource) @@ -39,7 +40,8 @@ namespace bpkg return nn > sn && n.compare (nn - sn, sn, s) == 0; }; - auto parse_group = [&split, &suffix] (const string& g) + auto parse_group = [&split, &suffix] (const string& g, + const package_name* pn) { strings ns (split (g, ' ')); @@ -48,8 +50,10 @@ namespace bpkg package_status r; - // Handle the devel instead of main special case for libraries. + // Handle the "devel instead of main" special case for libraries. // + // Note: the lib prefix check is based on the bpkg package name. + // // Check that the following name does not end with -devel. This will be // the only way to disambiguate the case where the library name happens // to end with -devel (e.g., libops-devel libops-devel-devel). @@ -57,7 +61,9 @@ namespace bpkg { string& m (ns[0]); - if (suffix (m, "-devel") && + if (pn != nullptr && + pn->string ().compare (0, 3, "lib") == 0 && + suffix (m, "-devel") && !(ns.size () > 1 && suffix (ns[1], "-devel"))) { r = package_status ("", move (m)); @@ -100,10 +106,10 @@ namespace bpkg for (size_t i (0); i != gs.size (); ++i) { if (i == 0) // Main group. - r = parse_group (gs[i]); + r = parse_group (gs[i], &pn); else { - package_status g (parse_group (gs[i])); + package_status g (parse_group (gs[i], nullptr)); if (!g.main.empty ()) r.extras.push_back (move (g.main)); if (!g.devel.empty ()) r.extras.push_back (move (g.devel)); @@ -126,6 +132,41 @@ namespace bpkg return r; } + // Attempt to determine the main package name from its -devel package based + // on the extracted dependencies. Return empty string if unable to. + // + string system_package_manager_fedora:: + main_from_dev (const string& devel_name, + const string& devel_ver, + const vector>& depends) + { + // For the main package we look for a dependency with the name + // and the devel_ver version. Failed that, try the + // -libs name instead. + // + string devel_stem (devel_name, 0, devel_name.rfind ("-devel")); + + auto find = [&devel_ver, &depends] (const string& n) + { + auto i (find_if (depends.begin (), depends.end (), + [&n, &devel_ver] (const pair& d) + { + return d.first == n && d.second == devel_ver; + })); + + return i != depends.end () ? i->first : empty_string; + }; + + // Note that for a mixed package we need to rather end up with the -libs + // subpackage rather that with the base package. Think of the following + // package: + // + // openssl openssl-libs openssl-devel + // + string r (find (devel_stem + "-libs")); + return !r.empty () ? r : find (devel_stem); + } + static process_path dnf_path; static process_path sudo_path; @@ -135,35 +176,35 @@ namespace bpkg // If the n argument is not 0, then only query the first n packages. // void system_package_manager_fedora:: - dnf_list (vector& pps, size_t n) + dnf_list (vector& pis, size_t n) { if (n == 0) - n = pps.size (); + n = pis.size (); - assert (n != 0 && n <= pps.size ()); + assert (n != 0 && n <= pis.size ()); - // In particular, --quiet makes sure we don't get 'Last metadata - // expiration check: ' printed to stderr. It does not appear to - // affect error diagnostics (try specifying an unknown package). + // The --quiet option makes sure we don't get 'Last metadata expiration + // check: ' printed to stderr. It does not appear to affect + // error diagnostics (try specifying an unknown package). // cstrings args { "dnf", "list", "--all", - "--cacheonly" + "--cacheonly", "--quiet"}; for (size_t i (0); i != n; ++i) { - package_policy& pp (pps[i]); + package_info& pi (pis[i]); - string& n (pp.name); + string& n (pi.name); assert (!n.empty ()); - pp.installed_version.clear (); - pp.candidate_version.clear (); + pi.installed_version.clear (); + pi.candidate_version.clear (); - n += '.'; - n += host_.cpu; + pi.installed_arch.clear (); + pi.candidate_arch.clear (); args.push_back (n.c_str ()); } @@ -268,6 +309,10 @@ namespace bpkg print_process (dr, pe, args); }); + // If true, then we inside the 'Installed Packages' section. If + // false, then we inside the 'Available Packages' section. Initially + // nullopt. + // optional installed; for (string l; !eof (getline (is, l)); ) { @@ -315,27 +360,57 @@ namespace bpkg string v (l, b, e - b); // While we don't really care about the rest of the line, let's - // for good measure verify that it also contains a repository id. + // verify that it also contains a repository id, for good measure. // b = l.find_first_not_of (' ', e); if (b == string::npos) fail << "expected package repository in '" << l << "'"; + // Skip the special dnf package. + // if (p == "dnf.noarch") continue; - // Find the package. + // Separate the architecture from the package name. + // + e = p.rfind ('.'); + + if (e == string::npos || e == p.size () - 1) + fail << "can't deduce architecture for package '" << p + << "' in '" << l << "'"; + + string a (p, e + 1); + + // Skip the package of a different architecture. + // + if (a != host_.cpu && a != "noarch") + continue; + + p.resize (e); + + // Find the package info to update. // - auto i (find_if (pps.begin (), pps.end (), - [&p] (const package_policy& pp) - {return pp.name == p;})); + auto i (find_if (pis.begin (), pis.end (), + [&p] (const package_info& pi) + {return pi.name == p;})); - if (i == pps.end ()) + if (i == pis.end ()) fail << "unexpected package name '" << p << "' in '" << l << "'"; - (*installed ? i->installed_version : i->candidate_version) = - move (v); + string& ver (*installed + ? i->installed_version + : i->candidate_version); + + if (!ver.empty ()) + fail << "multiple " << (*installed ? "installed " : "available ") + << "versions of package '" << p << "'" << + info << "first: " << ver << + info << "second: " << v; + + ver = move (v); + + (*installed ? i->installed_arch : i->candidate_arch) = move (a); } } @@ -380,11 +455,19 @@ namespace bpkg // returned. // vector> system_package_manager_fedora:: - dnf_repoquery_requires (const string& name, const string& ver) + dnf_repoquery_requires (const string& name, + const string& ver, + const string& arch) { assert (!name.empty () && !ver.empty ()); - string spec (name + '-' + ver); + // Qualify the package with an architecture suffix. + // + // Note that by reason unknown, the below command may also print + // dependency packages of different architectures. It feels sensible to + // just skip them. + // + string spec (name + '-' + ver + '.' + arch); // In particular, --quiet makes sure we don't get 'Last metadata // expiration check: ' printed to stderr. It does not appear to @@ -393,8 +476,7 @@ namespace bpkg const char* args[] = { "dnf", "repoquery", "--requires", "--resolve", - "--arch", host_.cpu.c_str (), - "--qf", "%{name} %{version}-%{release}", + "--qf", "%{name} %{arch} %{epoch}:%{version}-%{release}", "--cacheonly", "--quiet", spec.c_str (), @@ -473,35 +555,61 @@ namespace bpkg { ifdstream is (move (pr.in_ofd), fdstream_mode::skip, ifdstream::badbit); - // The output of `dnf repoquery --requires -` will be the - // sequence of the dependency package lines in the ` ` - // form. Here is a representative example: + // The output of the command will be the sequence of the dependency + // package lines in the ` ` form. So for example + // for the libicu-devel-69.1-6.fc35.x86_64 package it is as follows: // - // bash 5.1.8-3.fc35 - // glibc 2.34-49.fc35 - // libicu 69.1-6.fc35 - // libicu-devel 69.1-6.fc35 - // pkgconf-pkg-config 1.8.0-1.fc35 + // bash i686 0:5.1.8-3.fc35 + // bash x86_64 0:5.1.8-3.fc35 + // glibc i686 0:2.34-49.fc35 + // glibc x86_64 0:2.34-49.fc35 + // libicu x86_64 0:69.1-6.fc35 + // libicu-devel i686 0:69.1-6.fc35 + // libicu-devel x86_64 0:69.1-6.fc35 + // pkgconf-pkg-config i686 0:1.8.0-1.fc35 + // pkgconf-pkg-config x86_64 0:1.8.0-1.fc35 // for (string l; !eof (getline (is, l)); ) { - size_t p (l.find (' ')); + size_t e (l.find (' ')); + + if (l.empty () || e == 0) + fail << "expected package name in '" << l << "'"; + + if (e == string::npos) + fail << "expected package architecture in '" << l << "'"; - if (p == string::npos) - fail << "expected package name and version instead of '" << l - << "'"; + string p (l, 0, e); - // Split the line into the package name and version. + size_t b (e + 1); + e = l.find (' ', b); + + if (e == string::npos) + fail << "expected package version in '" << l << "'"; + + string a (l, b, e - b); + if (a.empty ()) + fail << "expected package architecture in '" << l << "'"; + + string v (l, e + 1); + + // Strip the '0:' epoch to align with package versions retrieved by + // other functions (dnf_list(), etc). // - string v (l, p + 1); - l.resize (p); // Name. + e = v.find (':'); + if (e == string::npos || e == 0) + fail << "no epoch for package version in '" << l << "'"; + + if (e == 1 && v[0] == '0') + v.erase (0, 2); - // Skip the potential self-dependency line (see the above example). + // Skip the potential self-dependency line (see the above example) + // and dependencies of a different architecture. // - if (l == name && v == ver) + if (l == name || (a != host_.cpu && a != "noarch")) continue; - r.emplace_back (move (l), move (v)); + r.emplace_back (move (p), move (v)); } is.close (); @@ -539,13 +647,285 @@ namespace bpkg return r; } + // Prepare the common options for `dnf makecache` and `dnf install` + // commands. + // + pair system_package_manager_fedora:: + dnf_common (const char* command) + { + cstrings args; + + if (!sudo_.empty ()) + args.push_back (sudo_.c_str ()); + + args.push_back ("dnf"); + args.push_back (command); + + // Map our verbosity/progress to dnf --quiet and --verbose options. + // + // Note that all the diagnostics, including the progress indication but + // excluding error messages, is printed to stdout. By default the progress + // bar for network transfers is printed, unless stdout is not a terminal. + // The --quiet option disables printing the plan and all the progress + // output, but not the confirmation prompt nor error messages. + // + if (progress_ && *progress_) + { + // Print the progress bar by default, unless this is not a terminal. + } + else if (verb == 0) + { + args.push_back ("--quiet"); + } + else if (verb > 3) + { + args.push_back ("--verbose"); + } + else if (progress_ && !*progress_) + { + args.push_back ("--quiet"); + } + + if (yes_) + { + args.push_back ("--assumeyes"); + } + else if (!stderr_term) + { + // Suppress any prompts if stderr is not a terminal for good measure. + // + args.push_back ("--assumeno"); + } + + try + { + const process_path* pp (nullptr); + + if (!sudo_.empty ()) + { + if (sudo_path.empty () && !simulate_) + sudo_path = process::path_search (args[0]); + + pp = &sudo_path; + } + else + { + if (dnf_path.empty () && !simulate_) + dnf_path = process::path_search (args[0]); + + pp = &dnf_path; + } + + return pair (move (args), *pp); + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + throw failed (); + } + } + + // Execute `dnf makecache` to download and cache the repositories metadata. + // + void system_package_manager_fedora:: + dnf_makecache () + { + pair args_pp (dnf_common ("makecache")); + + cstrings& args (args_pp.first); + const process_path& pp (args_pp.second); + + args.push_back ("--refresh"); + args.push_back (nullptr); + + try + { + if (verb >= 2) + print_process (args); + else if (verb == 1) + text << "updating " << os_release_.name_id + << " repositories metadata..."; + + process pr; + if (!simulate_) + pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); + else + { + print_process (args); + // @@ TODO + //pr = process (process_exit (simulate_->apt_get_update_fail_ ? 100 : 0)); + } + + if (!pr.wait ()) + { + diag_record dr (fail); + dr << "dnf makecache exited with non-zero code"; + + if (verb < 2) + { + dr << info << "command line: "; + print_process (dr, args); + } + } + + if (verb == 1) + text << "updated " << os_release_.name_id << " repositories metadata"; + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + throw failed (); + } + } + + // Execute `dnf install` to install the specified packages/versions (e.g., + // libfoo or libfoo-1.2.3) and the `dnf mark install` to mark the specified + // packages as installed by user. + // + void system_package_manager_fedora:: + dnf_install (const strings& pkgs) + { + assert (!pkgs.empty ()); + + // Install. + // + { + pair args_pp (dnf_common ("install")); + + cstrings& args (args_pp.first); + const process_path& pp (args_pp.second); + + // Note that we can't use --cacheonly here to prevent the metadata + // update, since the install command expects the package RPM files to + // also be cached then and fails if that's not the case. Thus we + // override the metadata_expire=never configuration option instead. + // + args.push_back ("--setopt=metadata_expire=never"); + + for (const string& p: pkgs) + args.push_back (p.c_str ()); + + args.push_back (nullptr); + + try + { + if (verb >= 2) + print_process (args); + else if (verb == 1) + text << "installing " << os_release_.name_id << " packages..."; + + process pr; + if (!simulate_) + pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); + else + { + print_process (args); + // @@ TODO + //pr = process (process_exit (simulate_->apt_get_install_fail_ ? 100 : 0)); + } + + if (!pr.wait ()) + { + diag_record dr (fail); + dr << "dnf install exited with non-zero code"; + + if (verb < 2) + { + dr << info << "command line: "; + print_process (dr, args); + } + + dr << info << "consider resolving the issue manually and retrying " + << "the bpkg command"; + } + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + throw failed (); + } + } + + // Mark as installed. + // + { + pair args_pp (dnf_common ("mark")); + + cstrings& args (args_pp.first); + const process_path& pp (args_pp.second); + + args.push_back ("install"); + args.push_back ("--cacheonly"); + + for (const string& p: pkgs) + args.push_back (p.c_str ()); + + args.push_back (nullptr); + + try + { + if (verb >= 2) + print_process (args); + + process pr; + if (!simulate_) + pr = process (pp, args, 0 /* stdin */, 2 /* stdout */); + else + { + print_process (args); + // @@ TODO + //pr = process (process_exit (simulate_->apt_get_install_fail_ ? 100 : 0)); + } + + if (!pr.wait ()) + { + diag_record dr (fail); + dr << "dnf mark install exited with non-zero code"; + + if (verb < 2) + { + dr << info << "command line: "; + print_process (dr, args); + } + + dr << info << "consider resolving the issue manually and retrying " + << "the bpkg command"; + } + + if (verb == 1) + text << "installed " << os_release_.name_id << " packages"; + } + catch (const process_error& e) + { + error << "unable to execute " << args[0] << ": " << e; + + if (e.child) + exit (1); + + throw failed (); + } + } + } + optional system_package_manager_fedora:: pkg_status (const package_name& pn, const available_packages* aps) { // For now we ignore -doc and -debug* package components (but we may want // to have options controlling this later). Note also that we assume // -common is pulled automatically by the base package so we ignore it as - // well. + // well (see equivalent logic in parse_name_value()). // bool need_doc (false); bool need_debuginfo (false); @@ -582,44 +962,35 @@ namespace bpkg if (ns.empty ()) { // Attempt to automatically translate our package name (see above for - // details). + // details). Failed that we should try to use the project name, if + // present, instead. // const string& n (pn.string ()); + assert (!aps->empty ()); + + const shared_ptr& ap (aps->front ().first); + string p (ap->project && *ap->project != n + ? ap->project->string () + : empty_string); + // The best we can do in trying to detect whether this is a library is // to check for the lib prefix. Libraries without the lib prefix and // non-libraries with the lib prefix (both of which we do not // recomment) will have to provide a manual mapping. // - // @@ We should probably to also/instead consider the project name. We - // will need to add it to available_package type then and take it - // from the latest available package. - // - // const string* prj (aps != nullptr && aps->front ().project - // ? &aps->front ().project->string () - // : nullptr); - // if (n.compare (0, 3, "lib") == 0) { + if (!p.empty ()) + p += "-devel"; + // Keep the base package name empty as an indication that it is to // be discovered. // - candidates.push_back (package_status ("", n + "-devel")); - - // @@ Add the project-based candidate. - // - // if (prj != nullptr) - // candidates.push_back (package_status ("", *prj + "-devel")); + candidates.push_back (package_status ("", n + "-devel", move (p))); } else - { - candidates.push_back (package_status (n)); - - // @@ Add the project-based candidate. - // - // if (prj != nullptr) - // candidates.push_back (package_status ("", *prj)); - } + candidates.push_back (package_status (n, "", move (p))); } else { @@ -627,8 +998,11 @@ namespace bpkg // for (const string& n: ns) { - package_status s ( - parse_name_value (n, need_doc, need_debuginfo, need_debugsource)); + package_status s (parse_name_value (pn, + n, + need_doc, + need_debuginfo, + need_debugsource)); // Suppress duplicates for good measure based on the base package // name (and falling back to -devel if empty). @@ -636,108 +1010,417 @@ namespace bpkg auto i (find_if (candidates.begin (), candidates.end (), [&s] (const package_status& x) { - return s.main.empty () - ? s.devel == x.devel - : s.main == x.main; + // Note that it's possible for one mapping to be + // specified as -devel only while the other as + // main and -devel. + // + return s.main.empty () || x.main.empty () + ? s.devel == x.devel + : s.main == x.main; })); if (i == candidates.end ()) candidates.push_back (move (s)); else { - // @@ Should we verify the rest matches for good measure? + // Should we verify the rest matches for good measure? But what if + // we need to override, as in: + // + // fedora_35-name: libfoo libfoo-bar-dev + // fedora_34-name: libfoo libfoo-dev + // + // Note that for this to work we must get fedora_35 values before + // fedora_34, which is the semantics guaranteed by + // system_package_names(). } } } } - // Guess unknown main package given the devel package and its version. + // Guess unknown main package given the -devel package and its version. // - auto guess_main = [this, &pn] (package_status& s, const string& ver) + auto guess_main = [this, &pn] (package_status& s, + const string& ver, + const string& arch) { vector> depends ( - dnf_repoquery_requires (s.devel, ver)); -#if 0 - s.main = main_from_dev (s.dev, ver, depends); + dnf_repoquery_requires (s.devel, ver, arch)); + + s.main = main_from_dev (s.devel, ver, depends); if (s.main.empty ()) { - fail << "unable to guess main Debian package for " << s.dev << ' ' - << ver << - info << s.dev << " Depends value: " << depends << - info << "consider specifying explicit mapping in " << pn - << " package manifest"; + diag_record dr (fail); + dr << "unable to guess main " << os_release_.name_id + << " package for " << s.devel << ' ' << ver << + info << "depends on"; + + bool first (true); + for (const pair& d: depends) + { + if (first) + first = false; + else + dr << ','; + + dr << d.first << ' ' << d.second; + } + + dr << info << "consider specifying explicit mapping in " << pn + << " package manifest"; } -#endif }; - // First look for an already fully installed package. + // Calculate the package status from individual package components. + // Return nullopt if there is a component without installed or candidate + // version (which means the package cannot be installed). // - optional r; + // The main argument specifies the size of the main group. Only components + // from this group are considered for partially_installed determination. + // + // @@ TODO: we should probably prioritize partially installed with fully + // installed main group. Add almost_installed next to partially_installed? + // + using status_type = package_status::status_type; + + auto status = [] (const vector& pis, size_t main) + -> optional + { + bool i (false), u (false); + + for (size_t j (0); j != pis.size (); ++j) + { + const package_info& pi (pis[j]); + + if (pi.installed_version.empty ()) + { + if (pi.candidate_version.empty ()) + return nullopt; + + u = true; + } + else if (j < main) + i = true; + } + + return (!u ? package_status::installed : + !i ? package_status::not_installed : + package_status::partially_installed); + }; + // First look for an already fully installed package. + // + // But first, choose between the package name-based and project-based + // guessed system package name. + // for (package_status& ps: candidates) { - vector& pps (ps.package_policies); + vector& pis (ps.package_infos); - if (!ps.main.empty ()) pps.emplace_back (ps.main); - if (!ps.devel.empty ()) pps.emplace_back (ps.devel); - if (!ps.doc.empty () && need_doc) pps.emplace_back (ps.doc); + if (!ps.main.empty ()) pis.emplace_back (ps.main); + if (!ps.devel.empty ()) pis.emplace_back (ps.devel); + if (!ps.fallback.empty ()) pis.emplace_back (ps.fallback); + if (!ps.doc.empty () && need_doc) pis.emplace_back (ps.doc); if (!ps.debuginfo.empty () && need_debuginfo) - pps.emplace_back (ps.debuginfo); + pis.emplace_back (ps.debuginfo); if (!ps.debugsource.empty () && need_debugsource) - pps.emplace_back (ps.debugsource); + pis.emplace_back (ps.debugsource); - if (!ps.common.empty () && false) pps.emplace_back (ps.common); - ps.package_policies_main = pps.size (); - for (const string& n: ps.extras) pps.emplace_back (n); + if (!ps.common.empty () && false) pis.emplace_back (ps.common); + ps.package_infos_main = pis.size (); + for (const string& n: ps.extras) pis.emplace_back (n); - dnf_list (pps); + dnf_list (pis); - // Handle the unknown main package. + // If the (project-based) fallback system package name is specified, + // then choose between the guessed and fallback names depending on which + // of them is known to the system package manager. + // + // Specifically, if the guessed system package exists we use that. + // Otherwise, if the fallback system package exists we use that and fail + // otherwise. // - if (ps.main.empty ()) + if (!ps.fallback.empty ()) { - const package_policy& devel (pps.front ()); + assert (pis.size () > 1); // devel, fallback,... or main, fallback,... - // Note that at this stage we can only use the installed devel package - // (since the candidate version may change after fetch). + // Either devel or main is guessed. // - if (devel.installed_version.empty ()) - continue; + bool guessed_devel (!ps.devel.empty ()); + assert (guessed_devel == ps.main.empty ()); + + string& guessed (guessed_devel ? ps.devel : ps.main); + + package_info& gi (pis[0]); // Guessed package info. + package_info& fi (pis[1]); // Fallback package info. + + if (gi.unknown ()) + { + if (fi.known ()) + { + guessed = move (ps.fallback); + gi = move (fi); + } + else + { + fail << "unable to guess " << (guessed_devel ? "devel" : "main") + << ' ' << os_release_.name_id << " package for " << pn << + info << "neither " << guessed << " nor " << ps.fallback + << ' ' << os_release_.name_id << " package exists" << + info << "consider specifying explicit mapping in " << pn + << " package manifest"; + + } + } - guess_main (ps, devel.installed_version); - pps.emplace (pps.begin (), ps.main); - ps.package_policies_main++; - dnf_list (pps, 1); + // Whether it was used or not, cleanup the fallback information. + // + ps.fallback.clear (); + pis.erase (pis.begin () + 1); + --ps.package_infos_main; } + } -#if 0 - optional s (status (pps, ps.package_policies_main)); + optional r; - if (!s) - continue; + { + diag_record dr; // Ambiguity diagnostics. - if (*s == package_status::installed) + for (package_status& ps: candidates) { - const package_policy& main (pps.front ()); + vector& pis (ps.package_infos); + + // Handle the unknown main package. + // + if (ps.main.empty ()) + { + const package_info& devel (pis.front ()); + + // Note that at this stage we can only use the installed -devel + // package (since the candidate version may change after fetch). + // + if (devel.installed_version.empty ()) + continue; + + guess_main (ps, devel.installed_version, devel.installed_arch); + pis.emplace (pis.begin (), ps.main); + ps.package_infos_main++; + dnf_list (pis, 1); + } + + optional s (status (pis, ps.package_infos_main)); + + if (!s || *s != package_status::installed) + continue; + + const package_info& main (pis.front ()); ps.status = *s; ps.system_name = main.name; ps.system_version = main.installed_version; - if (r) + if (!r) + { + r = move (ps); + continue; + } + + if (dr.empty ()) + { + dr << fail << "multiple installed " << os_release_.name_id + << " packages for " << pn << + info << "candidate: " << r->main << " " << r->system_version; + } + + dr << info << "candidate: " << ps.main << " " << ps.system_version; + } + + if (!dr.empty ()) + dr << info << "consider specifying the desired version manually"; + } + + // Next look for available versions if we are allowed to install. + // + if (!r && install_) + { + // If we weren't instructed to fetch or we already fetched, then we + // don't need to re-run dnf_list(). + // + bool requery; + if ((requery = fetch_ && !fetched_)) + { + dnf_makecache (); + fetched_ = true; + } + + { + diag_record dr; // Ambiguity diagnostics. + + for (package_status& ps: candidates) + { + vector& pis (ps.package_infos); + + if (requery) + dnf_list (pis); + + // Handle the unknown main package. + // + if (ps.main.empty ()) + { + const package_info& devel (pis.front ()); + + // Note that this time we use the candidate version. + // + if (devel.candidate_version.empty ()) + continue; // Not installable. + + guess_main (ps, devel.candidate_version, devel.candidate_arch); + pis.emplace (pis.begin (), ps.main); + ps.package_infos_main++; + dnf_list (pis, 1); + } + + optional s (status (pis, ps.package_infos_main)); + + if (!s) + { + ps.main.clear (); // Not installable. + continue; + } + + assert (*s != package_status::installed); // Sanity check. + + const package_info& main (pis.front ()); + + // Note that if we are installing something for this main package, + // then we always go for the candidate version even though it may + // have an installed version that may be good enough (especially if + // what we are installing are extras). The reason is that it may as + // well not be good enough (especially if we are installing the -dev + // package) and there is no straightforward way to change our mind. + // + ps.status = *s; + ps.system_name = main.name; + ps.system_version = main.candidate_version; + + // Prefer partially installed to not installed. This makes detecting + // ambiguity a bit trickier so we handle partially installed here + // and not installed in a separate loop below. + // + if (ps.status != package_status::partially_installed) + continue; + + if (!r) + { + r = move (ps); + continue; + } + + auto print_missing = [&dr] (const package_status& s) + { + for (const package_info& pi: s.package_infos) + if (pi.installed_version.empty ()) + dr << ' ' << pi.name; + }; + + if (dr.empty ()) + { + dr << fail << "multiple partially installed " + << os_release_.name_id << " packages for " << pn; + + dr << info << "candidate: " << r->main << " " << r->system_version + << ", missing components:"; + print_missing (*r); + } + + dr << info << "candidate: " << ps.main << " " << ps.system_version + << ", missing components:"; + print_missing (ps); + } + + if (!dr.empty ()) + dr << info << "consider fully installing the desired package " + << "manually and retrying the bpkg command"; + } + + if (!r) + { + diag_record dr; // Ambiguity diagnostics. + + for (package_status& ps: candidates) { - fail << "multiple installed " << os_release_.name_id + if (ps.main.empty ()) + continue; + + assert (ps.status == package_status::not_installed); // Sanity check. + + if (!r) + { + r = move (ps); + continue; + } + + if (dr.empty ()) + { + dr << fail << "multiple available " << os_release_.name_id << " packages for " << pn << - info << "first package: " << r->main << " " << r->system_version << - info << "second package: " << ps.main << " " << ps.system_version << - info << "consider specifying the desired version manually"; + info << "candidate: " << r->main << " " << r->system_version; + } + + dr << info << "candidate: " << ps.main << " " << ps.system_version; } - r = move (ps); + if (!dr.empty ()) + dr << info << "consider installing the desired package manually and " + << "retrying the bpkg command"; } -#endif + } + + if (r) + { + // Map the Fedora version to the bpkg version. But first strip the + // revision from Fedora version ([:]-). + // + // Note that according to deb-version(5), may contain `:`/`-` + // but in these cases / must be specified explicitly, + // respectively. + // + string sv (r->system_version, 0, r->system_version.rfind ('-')); + + optional v ( + downstream_package_version (sv, + *aps, + os_release_.name_id, + os_release_.version_id, + os_release_.like_ids)); + + if (!v) + { + // Fallback to using system version as downstream version. But first + // strip the epoch, if any. + // + size_t p (sv.find (':')); + if (p != string::npos) + sv.erase (0, p + 1); + + try + { + v = version (sv); + } + catch (const invalid_argument& e) + { + fail << "unable to map " << os_release_.name_id << " package " + << r->system_name << " version " << sv << " to bpkg package " + << pn << " version" << + info << os_release_.name_id << " version is not a valid bpkg " + << "version: " << e.what () << + info << "consider specifying explicit mapping in " << pn + << " package manifest"; + } + } + + r->version = move (*v); } // Cache. @@ -747,8 +1430,141 @@ namespace bpkg } void system_package_manager_fedora:: - pkg_install (const vector& /*pns*/) + pkg_install (const vector& pns) { - // @@ TODO + assert (!pns.empty ()); + + assert (install_ && !installed_); + installed_ = true; + + // Collect and merge all the Fedora packages/version for the specified + // bpkg packages. + // + struct package + { + string name; + string version; // Empty if unspecified. + }; + vector pkgs; + + for (const package_name& pn: pns) + { + auto it (status_cache_.find (pn)); + assert (it != status_cache_.end () && it->second); + + const package_status& ps (*it->second); + + // @@ Amend. + // + // At first it may seem we don't need to do anything for already fully + // installed packages. But it's possible some of them were automatically + // installed, meaning that they can be automatically removed if they no + // longer have any dependents (see dnf(8) for details). Which in + // turn means that things may behave differently depending on whether + // we've installed a package ourselves or if it was already installed. + // So instead we are going to also pass the already fully installed + // packages which will make sure they are all set to manually installed. + // But we must be careful not to force their upgrade. To achieve this + // we will specify the installed version as the desired version. + // + // Note also that for partially/not installed we don't specify the + // version, expecting the candidate version to be installed. + // + bool fi (ps.status == package_status::installed); + + for (const package_info& pi: ps.package_infos) + { + string n (pi.name); + string v (fi ? pi.installed_version : string ()); + + auto i (find_if (pkgs.begin (), pkgs.end (), + [&n] (const package& p) + { + return p.name == n; + })); + + if (i != pkgs.end ()) + { + if (i->version.empty ()) + i->version = move (v); + else + // Feels like this cannot happen since we always use the installed + // version of the package. + // + assert (i->version == v); + } + else + pkgs.push_back (package {move (n), move (v)}); + } + } + + // Install. + // + { + // Convert to the `dnf install` [-] form. + // + strings specs; + specs.reserve (pkgs.size ()); + for (const package& p: pkgs) + { + string s (p.name); + if (!p.version.empty ()) + { + s += '-'; + s += p.version; + } + specs.push_back (move (s)); + } + + dnf_install (specs); + } + + // Verify that versions we have promised in pkg_status() match what + // actually got installed. + // + { + vector pis; + + // Here we just check the main package component of each package. + // + for (const package_name& pn: pns) + { + const package_status& ps (*status_cache_.find (pn)->second); + + if (find_if (pis.begin (), pis.end (), + [&ps] (const package_info& pi) + { + return pi.name == ps.system_name; + }) == pis.end ()) + { + pis.push_back (package_info (ps.system_name)); + } + } + + dnf_list (pis); + + for (const package_name& pn: pns) + { + const package_status& ps (*status_cache_.find (pn)->second); + + auto i (find_if (pis.begin (), pis.end (), + [&ps] (const package_info& pi) + { + return pi.name == ps.system_name; + })); + assert (i != pis.end ()); + + const package_info& pi (*i); + + if (pi.installed_version != ps.system_version) + { + fail << "unexpected " << os_release_.name_id << " package version " + << "for " << ps.system_name << + info << "expected: " << ps.system_version << + info << "installed: " << pi.installed_version << + info << "consider retrying the bpkg command"; + } + } + } } } diff --git a/bpkg/system-package-manager-fedora.hxx b/bpkg/system-package-manager-fedora.hxx index 129a9d2..8047836 100644 --- a/bpkg/system-package-manager-fedora.hxx +++ b/bpkg/system-package-manager-fedora.hxx @@ -14,6 +14,8 @@ namespace bpkg // The system package manager implementation for Fedora and alike (Red Hat // Enterprise Linux, CentOS, etc) using the DNF frontend. // + // NOTE: the below description is also reproduced in the bpkg manual. + // // For background, a library in Fedora is normally split up into several // packages: the shared library package (e.g., libfoo), the development // files package (e.g., libfoo-devel), the static library package (e.g., @@ -27,7 +29,7 @@ namespace bpkg // name has it (see some examples below). // // For mixed packages which include both applications and libraries, the - // shared library package normally have the -libs suffix (e.g., foo-libs). + // shared library package normally has the -libs suffix (e.g., foo-libs). // // A package name may also include an upstream version based suffix if // multiple versions of the package can be installed simultaneously (e.g., @@ -51,7 +53,7 @@ namespace bpkg // // icu libicu libicu-devel libicu-doc // - // openssl openssl-devel openssl-libs + // openssl openssl-libs openssl-devel // // curl libcurl libcurl-devel // @@ -73,7 +75,7 @@ namespace bpkg // For application packages there is normally no -devel packages but // -debug*, -doc, and -common are plausible. // - // The format of the fedora-name (or alike) manifest value value is a comma- + // The format of the fedora-name (or alike) manifest value is a comma- // separated list of one or more package groups: // // [, ...] @@ -110,12 +112,12 @@ namespace bpkg // The Fedora package version has the [:]- form // where the parts correspond to the Epoch (optional upstream versioning // scheme), Version (upstream version), and Release (Fedora's package - // revision) RPM tags (see the Fedora package Versioning Guidelines and RPM - // tags documentation for details). If no explicit mapping to bpkg version - // is specified with the fedora-to-downstream-version manifest values (or - // alike), then we fallback to using the part as bpkg version. If - // explicit mapping is specified, then we match it against the - // [:] parts ignoring . + // revision) RPM tags (see the Fedora Package Versioning Guidelines and RPM + // tags documentation for details). If no explicit mapping to the bpkg + // version is specified with the fedora-to-downstream-version (or alike) + // manifest values or none match, then we fallback to using the + // part as the bpkg version. If explicit mapping is specified, then we match + // it against the [:] parts ignoring . // struct system_package_status_fedora: system_package_status { @@ -127,28 +129,56 @@ namespace bpkg string common; strings extras; - string devel_fallback; // Fallback based on project name. + string fallback; // Fallback based on project name. - // @@ Rename. package_info? - // - // The `apt-cache policy` output. + // The `dnf list` output. // - struct package_policy + struct package_info { string name; string installed_version; // Empty if none. string candidate_version; // Empty if none and no installed_version. + // The installed/candidate package version architecture. Can be the host + // architecture or noarch. + // + // Note that in Fedora the same package version can be available for + // multiple architectures or be architecture-independent. For example: + // + // dbus-libs-1:1.12.22-1.fc35.i686 + // dbus-libs-1:1.12.22-1.fc35.x86_64 + // dbus-common-1:1.12.22-1.fc35.noarch + // code-insiders-1.75.0-1675123170.el7.armv7hl + // code-insiders-1.75.0-1675123170.el7.aarch64 + // code-insiders-1.75.0-1675123170.el7.x86_64 + // + // Thus, on package query you normally need to qualify the package with + // the architecture suffix or filter the query result, normally skipping + // packages which are specific for architecture other than the host + // architecture. + // + string installed_arch; + string candidate_arch; + explicit - package_policy (string n): name (move (n)) {} + package_info (string n): name (move (n)) {} + + bool + unknown () const + { + return installed_version.empty () && candidate_version.empty (); + } + + bool + known () const {return !unknown ();} }; - vector package_policies; - size_t package_policies_main = 0; // Size of the main group. + vector package_infos; + size_t package_infos_main = 0; // Size of the main group. explicit - system_package_status_fedora (string m, string d = {}) - : main (move (m)), devel (move (d)) + system_package_status_fedora (string m, string d = {}, string f = {}) + : main (move (m)), devel (move (d)), fallback (move (f)) { assert (!main.empty () || !devel.empty ()); } @@ -166,9 +196,8 @@ namespace bpkg pkg_install (const vector&) override; public: - // Note: expects os_release::name_id to be "fedora" or os_release::like_id - // to contain "fedora". - // + // Expects os_release::name_id to be "fedora" or os_release::like_ids to + // contain "fedora". using system_package_manager::system_package_manager; // Implementation details exposed for testing (see definitions for @@ -176,16 +205,30 @@ namespace bpkg // public: using package_status = system_package_status_fedora; - using package_policy = package_status::package_policy; + using package_info = package_status::package_info; void - dnf_list (vector&, size_t = 0); + dnf_list (vector&, size_t = 0); vector> - dnf_repoquery_requires (const string&, const string&); + dnf_repoquery_requires (const string&, const string&, const string&); + + void + dnf_makecache (); + + void + dnf_install (const strings&); + + pair + dnf_common (const char*); static package_status - parse_name_value (const string&, bool, bool, bool); + parse_name_value (const package_name&, const string&, bool, bool, bool); + + static string + main_from_dev (const string&, + const string&, + const vector>&); // @@ TODO // -- cgit v1.1