// 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 #include #include #include #include #include namespace bpkg { using os_release = butl::os_release; // The system/distribution package manager interface. Used by both pkg-build // (to query and install system packages) and by pkg-bindist (to generate // 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; }; // As mentioned above the system package manager API has two parts: // consumption (status() and install()) and production (generate()) and a // particular implementation may only implement one, the other, or both. If // a particular part is not implemented, then the correponding make_*() // function below should never return an instance of such a system package // manager. // 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 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 install (const vector&) = 0; // Generate a binary distribution package. See the pkg-bindist(1) man page // for background and the pkg_bindist() function implementation for // details. The recursive_full argument corresponds to the --recursive // auto (present false) and full (present true) modes. // // The available packages are loaded for all the packages in pkgs and // deps. For non-system packages (so for all in pkgs) there is always a // single available package that corresponds to the selected package. The // out_root is only set for packages in pkgs. Note also that all the // packages in pkgs and deps are guaranteed to belong to the same build // configuration (as opposed to being spread over multiple linked // configurations). Its absolute path is bassed in cfg_dir. // // The passed package manifest corresponds to the first package in pkgs // (normally used as a source of additional package metadata such as // summary, emails, urls, etc). // // The passed package type corresponds to the first package in pkgs while // the languages -- to all the packages in pkgs plus, in the recursive // mode, to all the non-system dependencies. In other words, the languages // list contains every language that is used by anything that ends up in // the package. // // Return the list of paths to binary packages and any other associated // files (build metadata, etc) that could be useful for their consumption. // Each returned file has a distribution-specific type that classifies it. // If the result is empty, assume the prepare-only mode (or similar) with // appropriate result diagnostics having been already issued. // // Note that this function may be called multiple times in the // --recursive=separate mode. In this case the first argument indicates // whether this is the first call (can be used, for example, to adjust the // --wipe-output semantics). // struct package { shared_ptr selected; available_packages available; dir_path out_root; // Absolute and normalized. }; using packages = vector; struct binary_file { string type; bpkg::path path; string system_name; // Empty if not applicable. }; struct binary_files: public vector { string system_version; // Empty if not applicable. }; virtual binary_files generate (const packages& pkgs, const packages& deps, const strings& vars, const dir_path& cfg_dir, const package_manifest&, const string& type, const small_vector&, optional recursive_full, bool first) = 0; public: bpkg::os_release os_release; target_triplet host; string arch; // Architecture in system package manager spelling. // Consumption constructor. // // 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. // // If fetch timeout (in seconds) is specified, then use it for all the // underlying network operations. // system_package_manager (bpkg::os_release&& osr, const target_triplet& h, string a, optional progress, optional fetch_timeout, bool install, bool fetch, bool yes, string sudo) : os_release (move (osr)), host (h), arch (move (a)), progress_ (progress), fetch_timeout_ (fetch_timeout), install_ (install), fetch_ (fetch), yes_ (yes), sudo_ (sudo != "false" ? move (sudo) : string ()) {} // Production constructor. // system_package_manager (bpkg::os_release&& osr, const target_triplet& h, string a, optional progress) : os_release (move (osr)), host (h), arch (move (a)), progress_ (progress), install_ (false), fetch_ (false), yes_ (false) {} 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 // is 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) // // The value in the _0 form is the special "non- // native" name mapping. If the native argument is false, then such a // mapping is preferred over any other mapping. If it is true, then such a // mapping is ignored. The purpose of this special value is to allow // specifying different package names for production compared to // consumption. Note, however, that such a deviation may make it // impossible to use native and non-native binary packages // interchangeably, for example, to satisfy dependencies. // // 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, bool native); // Given the available package and the repository fragment it belongs to, // return the system package version as mapped by one of the // -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 // -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 upstream/bpkg package version or // do something more elaborate). // // Note that lazy_shared_ptr is used only for // diagnostics and conveys the database the available package object // belongs to. // static optional system_package_version (const shared_ptr&, const lazy_shared_ptr&, 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); // Return the map of filesystem entries (files and symlinks) that would be // installed for the specified packages with the specified configuration // variables. // // In essence, this function runs: // // b --dry-run --quiet !config.install.scope= // !config.install.manifest=- install: // // And converts the printed installation manifest into the path map. // // Note that this function prints an appropriate progress indicator since // even in the dry-run mode it may take some time (see the --dry-run // option documentation for details). // struct installed_entry { string mode; // Empty if symlink. const pair* target; // Target if symlink. }; class installed_entry_map: public butl::path_map { public: // Return true if there are filesystem entries in the specified // directory or its subdirectories. // bool contains_sub (const dir_path& d) { auto p (find_sub (d)); return p.first != p.second; } }; installed_entry_map installed_entries (const common_options&, const packages& pkgs, const strings& vars, const string& scope); protected: optional progress_; // --[no]-progress (see also stderr_term) optional fetch_timeout_; // --fetch-timeout // The --sys-* option values. // bool install_; bool fetch_; bool yes_; string sudo_; }; // Create a package manager instance corresponding to the specified host // target triplet as well as optional distribution package manager name and // architecture. If name is empty, return NULL if there is no support for // this platform. If architecture is empty, then derive it automatically // from the host target triplet. Currently recognized names: // // debian -- Debian and alike (Ubuntu, etc) using the APT frontend. // fedora -- Fedora and alike (RHEL, Centos, etc) using the DNF frontend. // archive -- Installation archive, any platform, production only. // // Note: the name can be used to select an alternative package manager // implementation on platforms that support multiple. // unique_ptr make_consumption_system_package_manager (const common_options&, const target_triplet&, const string& name, const string& arch, bool install, bool fetch, bool yes, const string& sudo); // Create for production. The second half of the result is the effective // distribution name. // // Note that the reference to options is expected to outlive the returned // instance. // class pkg_bindist_options; pair, string> make_production_system_package_manager (const pkg_bindist_options&, const target_triplet&, const string& name, const string& arch); } #endif // BPKG_SYSTEM_PACKAGE_MANAGER_HXX