From 546391dab6173660acceba6404136e9411ce1388 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Wed, 1 Feb 2023 11:42:31 +0200 Subject: Implement system package manager query and install support for Debian --- bpkg/system-package-manager.hxx | 270 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 bpkg/system-package-manager.hxx (limited to 'bpkg/system-package-manager.hxx') diff --git a/bpkg/system-package-manager.hxx b/bpkg/system-package-manager.hxx new file mode 100644 index 0000000..9a9c443 --- /dev/null +++ b/bpkg/system-package-manager.hxx @@ -0,0 +1,270 @@ +// file : bpkg/system-package-manager.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_SYSTEM_PACKAGE_MANAGER_HXX +#define BPKG_SYSTEM_PACKAGE_MANAGER_HXX + +#include // version +#include + +#include +#include + +#include +#include +#include + +namespace bpkg +{ + // The system/distribution package manager interface. Used by both pkg-build + // (to query and install system packages) and by pkg-bindist (to build + // them). + // + // Note that currently the result of a query is a single available version. + // While some package managers may support having multiple available + // versions and may even allow installing multiple versions in parallel, + // supporting this on our side will complicate things quite a bit. While we + // can probably plug multiple available versions into our constraint + // satisfaction machinery, the rabbit hole goes deeper than that since, for + // example, different bpkg packages can be mapped to the same system + // package, as is the case for libcrypto/libssl which are both mapped to + // libssl on Debian. This means we will need to somehow coordinate (and + // likely backtrack) version selection between unrelated bpkg packages + // because only one underlying system version can be selected. (One + // simplified way to handle this would be to detect that different versions + // we selected and fail asking the user to resolve this manually.) + // + // Additionally, parallel installation is unlikely to be suppored for the + // packages we are interested in due to the underlying limitations. + // Specifically, the packages that we are primarily interested in are + // libraries with headers and executables (tools). While most package + // managers (e.g., Debian, Fedora) are able to install multiple libraries in + // parallel, they normally can only install a single set of headers, static + // libraries, pkg-config files, etc., (e.g., -dev/-devel package) at a time + // due to them being installed into the same location (e.g., /usr/include). + // The same holds for executables, which are installed into the same + // location (e.g., /usr/bin). + // + // It is possible that a certain library has made arrangements for + // multiple of its versions to co-exist. For example, hypothetically, our + // libssl package could be mapped to both libssl1.1 libssl1.1-dev and + // libssl3 libssl3-dev which could be installed at the same time (note + // that it is not the case in reality; there is only libssl-dev). However, + // in this case, we should probably also have two packages with separate + // names (e.g., libssl and libssl3) that can also co-exist. An example of + // this would be libQt5Core and libQt6Core. (Note that strictly speaking + // there could be different degrees of co-existence: for the system + // package manager it is sufficient for different versions not to clobber + // each other's files while for us we may also need the ability to use + // different versions in the base build). + // + // Note also that the above reasoning is quite C/C++-centric and it's + // possible that multiple versions of libraries (or equivalent) for other + // languages (e.g., Rust) can always co-exist. Plus, even in the case of + // C/C++ libraries, there is still the plausible case of picking one of + // the multiple available version. + // + // On the other hand, the ultimate goal of system package managers, at least + // traditional ones like Debian and Fedora, is to end up with a single, + // usually the latest available, version of the package that is used by + // everyone. In fact, if one looks at a stable distributions of Debian and + // Fedora, they normally provide only a single version of each package. This + // decision will also likely simplify the implementation. For example, on + // Debian, it's straightforward to get the installed and candidate versions + // (e.g., from apt-cache policy). But getting all the possible versions that + // can be installed without having to specify the release explicitly is a + // lot less straightforward (see the apt-cache command documentation in The + // Debian Administrator's Handbook for background). + // + // So for now we keep it simple and pick a single available version but can + // probably revise this decision later. + // + struct system_package_status + { + // Downstream (as in, bpkg package) version. + // + bpkg::version version; + + // System (as in, distribution) package name and version for diagnostics. + // + // Note that this status may represent multiple system packages (for + // example, libfoo and libfoo-dev) and here we have only the + // main/representative package name (for example, libfoo). + // + string system_name; + string system_version; + + // The system package can be either "available already installed", + // "available partially installed" (for example, libfoo but not + // libfoo-dev is installed) or "available not yet installed". + // + enum status_type {installed, partially_installed, not_installed}; + + status_type status = not_installed; + }; + + class system_package_manager + { + public: + // Query the system package status. + // + // This function has two modes: cache-only (available_packages is NULL) + // and full (available_packages is not NULL). In the cache-only mode this + // function returns the status of this package if it has already been + // queried and nullopt otherwise. This allows the caller to only collect + // all the available packages (for the name/version mapping information) + // if really necessary. + // + // The returned status can be NULL, which indicates that no such package + // is available from the system package manager. Note that NULL is also + // returned if no fully installed package is available from the system and + // package installation is not enabled (see the constructor below). + // + // Note also that the implementation is expected to issue appropriate + // progress and diagnostics if fetching package metadata (again see the + // constructor below). + // + virtual optional + pkg_status (const package_name&, const available_packages*) = 0; + + // Install the specified subset of the previously-queried packages. + // Should only be called if installation is enabled (see the constructor + // below). + // + // Note that this function should be called only once after the final set + // of the required system packages has been determined. And the specified + // subset should contain all the selected packages, including the already + // fully installed. This allows the implementation to merge and de- + // duplicate the system package set to be installed (since some bpkg + // packages may be mapped to the same system package), perform post- + // installation verifications (such as making sure the versions of already + // installed packages have not changed due to upgrades), change properties + // of already installed packages (e.g., mark them as manually installed in + // Debian), etc. + // + // Note also that the implementation is expected to issue appropriate + // progress and diagnostics. + // + virtual void + pkg_install (const vector&) = 0; + + public: + // If install is true, then enable package installation. + // + // If fetch is false, then do not re-fetch the system package repository + // metadata (that is, available packages/versions) before querying for the + // available version of the not yet installed or partially installed + // packages. + // + system_package_manager (os_release&& osr, + const target_triplet& host, + bool install, + bool fetch, + optional progress, + bool yes, + string sudo) + : os_release_ (osr), + host_ (host), + progress_ (progress), + install_ (install), + fetch_ (fetch), + yes_ (yes), + sudo_ (sudo != "false" ? move (sudo) : string ()) {} + + virtual + ~system_package_manager (); + + // Implementation details. + // + public: + // Given the available packages (as returned by find_available_all()) + // return the list of system package names as mapped by the + // -name values. + // + // The name_id, version_id, and like_ids are the values from os_release + // (refer there for background). If version_id is empty, then it's treated + // as "0". + // + // First consider -name values corresponding to name_id. + // Assume has the [_] form, where + // is a semver-like version (e.g, 10, 10.15, or 10.15.1) and return all + // the values that are equal or less than the specified version_id + // (include the value with the absent ). In a sense, absent + // can be treated as a 0 semver-like version. + // + // If no value is found then repeat the above process for every like_ids + // entry (from left to right) instead of name_id with version_id equal 0. + // + // If still no value is found, then return empty list (in which case the + // caller may choose to fallback to the downstream package name or do + // something more elaborate, like translate version_id to one of the + // like_id's version and try that). + // + // Note that multiple -name values per same distribution can be returned + // as, for example, for the following distribution values: + // + // debian_10-name: libcurl4 libcurl4-doc libcurl4-openssl-dev + // debian_10-name: libcurl3-gnutls libcurl4-gnutls-dev (yes, 3 and 4) + // + // Note also that the values are returned in the "override order", that is + // from the newest package version to oldest and then from the highest + // distribution version to lowest. + // + static strings + system_package_names (const available_packages&, + const string& name_id, + const string& version_id, + const vector& like_ids); + + // Given the system package version and available packages (as returned by + // find_available_all()) return the downstream package version as mapped + // by one of the -to-downstream-version values. + // + // The rest of the arguments as well as the overalls semantics is the same + // as in system_package_names() above. That is, first consider + // -to-downstream-version values corresponding to + // name_id. If none match, then repeat the above process for every + // like_ids entry with version_id equal 0. If still no match, then return + // nullopt (in which case the caller may choose to fallback to the system + // package version or do something more elaborate). + // + static optional + downstream_package_version (const string& system_version, + const available_packages&, + const string& name_id, + const string& version_id, + const vector& like_ids); + protected: + os_release os_release_; + target_triplet host_; + optional progress_; // --[no]-progress (see also stderr_term) + + // The --sys-* option values. + // + bool install_; + bool fetch_; + bool yes_; + string sudo_; + }; + + // Create a package manager instance corresponding to the specified host + // target and optional manager name. If name is empty, return NULL if there + // is no support for this platform. Currently recognized names: + // + // debian -- Debian and alike (Ubuntu, etc) using the APT frontend. + // fedora -- Fedora and alike (RHEL, Centos, etc) using the DNF frontend. + // + // Note: the name can be used to select an alternative package manager + // implementation on platforms that support multiple. + // + unique_ptr + make_system_package_manager (const common_options&, + const target_triplet&, + bool install, + bool fetch, + bool yes, + const string& sudo, + const string& name); +} + +#endif // BPKG_SYSTEM_PACKAGE_MANAGER_HXX -- cgit v1.1