From f1d08308522bbcbc655f6e55b4b84af9253d8679 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Thu, 9 Feb 2023 09:00:55 +0200 Subject: Infrastructure work for binary distribution package generation --- bpkg/package-query.hxx | 21 ++++- bpkg/pkg-build.cli | 19 ++++ bpkg/pkg-build.cxx | 16 ++-- bpkg/pkg-install.hxx | 1 - bpkg/pkg-uninstall.hxx | 1 - bpkg/pkg-update.hxx | 1 - bpkg/system-package-manager-debian.cxx | 123 ++++++++++++++++++++---- bpkg/system-package-manager-debian.hxx | 49 +++++++++- bpkg/system-package-manager-debian.test.cxx | 9 +- bpkg/system-package-manager-fedora.cxx | 79 ++++++++++------ bpkg/system-package-manager-fedora.hxx | 39 +++++++- bpkg/system-package-manager-fedora.test.cxx | 9 +- bpkg/system-package-manager.cxx | 141 +++++++++++++++++++++------- bpkg/system-package-manager.hxx | 89 ++++++++++++++---- bpkg/system-repository.hxx | 2 +- 15 files changed, 475 insertions(+), 124 deletions(-) diff --git a/bpkg/package-query.hxx b/bpkg/package-query.hxx index ee9b595..98bb7a0 100644 --- a/bpkg/package-query.hxx +++ b/bpkg/package-query.hxx @@ -154,7 +154,11 @@ namespace bpkg bool revision = false); // Try to find an available package corresponding to the specified selected - // package and, if not found, return a transient one. + // package and, if not found, return a transient one. The search is + // performed in the ultimate dependent configurations of the selected + // package (see dependent_repo_configs() for details). + // + // NOTE: repo_configs needs to be filled prior to the function call. // shared_ptr find_available (const common_options&, @@ -166,6 +170,8 @@ namespace bpkg // left empty and that the returned repository fragment could be NULL if the // package is an orphan. // + // NOTE: repo_configs needs to be filled prior to the function call. + // pair, lazy_shared_ptr> find_available_fragment (const common_options&, @@ -191,11 +197,17 @@ namespace bpkg // locations list is left empty and that the returned repository fragment // could be NULL if the package is an orphan. // - // Note also that in our model we assume that make_available_fragment() is + // Note that the repository fragment is searched in the ultimate dependent + // configurations of the selected package (see dependent_repo_configs() for + // details). + // + // Also note that in our model we assume that make_available_fragment() is // only called if there is no real available_package. This makes sure that // if the package moves (e.g., from testing to stable), then we will be // using stable to resolve its dependencies. // + // NOTE: repo_configs needs to be filled prior to the function call. + // pair, lazy_shared_ptr> make_available_fragment (const common_options&, @@ -230,6 +242,11 @@ namespace bpkg // Return the ultimate dependent configurations for packages in this // configuration. // + // Specifically, this is an intersection of all the dependent configurations + // for the specified configuration (see database::dependent_configs() for + // details) and configurations which contain repository information + // (repo_configs). + // linked_databases dependent_repo_configs (database&); } diff --git a/bpkg/pkg-build.cli b/bpkg/pkg-build.cli index 41a8432..28e4fa2 100644 --- a/bpkg/pkg-build.cli +++ b/bpkg/pkg-build.cli @@ -466,6 +466,25 @@ namespace bpkg with the \cb{--sys-install} option." } + string --sys-distribution + { + "", + "Alternative system/distribution package manager to interact with. The + valid values are \cb{debian} (Debian and alike, such as Ubuntu, + etc) and \cb{fedora} (Fedora and alike, such as RHEL, CentOS, etc). + Note that some package managers may only be supported when running on + certain host operating systems." + } + + string --sys-architecture + { + "", + "Alternative architecture to use when interacting with the system + package manager. The valid values are system/distribution + package manager-specific. If unspecified, the host architecture + is used." + } + dir_paths --directory|-d { "", diff --git a/bpkg/pkg-build.cxx b/bpkg/pkg-build.cxx index 9e5e1bb..6a4a4c3 100644 --- a/bpkg/pkg-build.cxx +++ b/bpkg/pkg-build.cxx @@ -1688,14 +1688,14 @@ namespace bpkg if (!sys_pkg_mgr) sys_pkg_mgr = o.sys_no_query () ? nullptr - : make_system_package_manager (o, - host_triplet, - o.sys_install (), - !o.sys_no_fetch (), - o.sys_yes (), - o.sys_sudo (), - "" /* name */); - + : make_consumption_system_package_manager (o, + host_triplet, + o.sys_distribution (), + o.sys_architecture (), + o.sys_install (), + !o.sys_no_fetch (), + o.sys_yes (), + o.sys_sudo ()); if (*sys_pkg_mgr != nullptr) { system_package_manager& spm (**sys_pkg_mgr); diff --git a/bpkg/pkg-install.hxx b/bpkg/pkg-install.hxx index 3898c1a..3f257f0 100644 --- a/bpkg/pkg-install.hxx +++ b/bpkg/pkg-install.hxx @@ -5,7 +5,6 @@ #define BPKG_PKG_INSTALL_HXX #include -#include // selected_package #include #include diff --git a/bpkg/pkg-uninstall.hxx b/bpkg/pkg-uninstall.hxx index c3df29a..6024fe1 100644 --- a/bpkg/pkg-uninstall.hxx +++ b/bpkg/pkg-uninstall.hxx @@ -5,7 +5,6 @@ #define BPKG_PKG_UNINSTALL_HXX #include -#include // selected_package #include #include diff --git a/bpkg/pkg-update.hxx b/bpkg/pkg-update.hxx index 3a2df8c..cac7651 100644 --- a/bpkg/pkg-update.hxx +++ b/bpkg/pkg-update.hxx @@ -5,7 +5,6 @@ #define BPKG_PKG_UPDATE_HXX #include -#include // selected_package #include #include diff --git a/bpkg/system-package-manager-debian.cxx b/bpkg/system-package-manager-debian.cxx index 4cad141..b131851 100644 --- a/bpkg/system-package-manager-debian.cxx +++ b/bpkg/system-package-manager-debian.cxx @@ -11,6 +11,19 @@ namespace bpkg { using package_status = system_package_status_debian; + // Translate host CPU to Debian package architecture. + // + string system_package_manager_debian:: + arch_from_target (const target_triplet& h) + { + const string& c (h.cpu); + return + c == "x86_64" ? "amd64" : + c == "aarch64" ? "arm64" : + c == "i386" || c == "i486" || c == "i586" || c == "i686" ? "i386" : + c; + } + // Parse the debian-name (or alike) value. // // Note that for now we treat all the packages from the non-main groups as @@ -757,7 +770,7 @@ namespace bpkg if (verb >= 2) print_process (args); else if (verb == 1) - text << "updating " << os_release_.name_id << " package index..."; + text << "updating " << os_release.name_id << " package index..."; process pr; if (!simulate_) @@ -781,7 +794,7 @@ namespace bpkg } if (verb == 1) - text << "updated " << os_release_.name_id << " package index"; + text << "updated " << os_release.name_id << " package index"; } catch (const process_error& e) { @@ -817,7 +830,7 @@ namespace bpkg if (verb >= 2) print_process (args); else if (verb == 1) - text << "installing " << os_release_.name_id << " packages..."; + text << "installing " << os_release.name_id << " packages..."; process pr; if (!simulate_) @@ -844,7 +857,7 @@ namespace bpkg } if (verb == 1) - text << "installed " << os_release_.name_id << " packages"; + text << "installed " << os_release.name_id << " packages"; } catch (const process_error& e) { @@ -889,15 +902,15 @@ namespace bpkg [this, &pn] (diag_record& dr) { dr << info << "while mapping " << pn << " to " - << os_release_.name_id << " package name"; + << os_release.name_id << " package name"; }); strings ns; if (!aps->empty ()) ns = system_package_names (*aps, - os_release_.name_id, - os_release_.version_id, - os_release_.like_ids); + os_release.name_id, + os_release.version_id, + os_release.like_ids); if (ns.empty ()) { // Attempt to automatically translate our package name (see above for @@ -970,7 +983,7 @@ namespace bpkg if (s.main.empty ()) { - fail << "unable to guess main " << os_release_.name_id + fail << "unable to guess main " << os_release.name_id << " package for " << s.dev << ' ' << ver << info << s.dev << " Depends value: " << depends << info << "consider specifying explicit mapping in " << pn @@ -1073,7 +1086,7 @@ namespace bpkg if (dr.empty ()) { - dr << fail << "multiple installed " << os_release_.name_id + dr << fail << "multiple installed " << os_release.name_id << " packages for " << pn << info << "candidate: " << r->main << " " << r->system_version; } @@ -1172,7 +1185,7 @@ namespace bpkg if (dr.empty ()) { dr << fail << "multiple partially installed " - << os_release_.name_id << " packages for " << pn; + << os_release.name_id << " packages for " << pn; dr << info << "candidate: " << r->main << " " << r->system_version << ", missing components:"; @@ -1208,7 +1221,7 @@ namespace bpkg if (dr.empty ()) { - dr << fail << "multiple available " << os_release_.name_id + dr << fail << "multiple available " << os_release.name_id << " packages for " << pn << info << "candidate: " << r->main << " " << r->system_version; } @@ -1238,9 +1251,9 @@ namespace bpkg if (!aps->empty ()) v = downstream_package_version (sv, *aps, - os_release_.name_id, - os_release_.version_id, - os_release_.like_ids); + os_release.name_id, + os_release.version_id, + os_release.like_ids); if (!v) { @@ -1257,10 +1270,10 @@ namespace bpkg } catch (const invalid_argument& e) { - fail << "unable to map " << os_release_.name_id << " package " + 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 " + info << os_release.name_id << " version is not a valid bpkg " << "version: " << e.what () << info << "consider specifying explicit mapping in " << pn << " package manifest"; @@ -1403,7 +1416,7 @@ namespace bpkg if (pp.installed_version != ps.system_version) { - fail << "unexpected " << os_release_.name_id << " package version " + fail << "unexpected " << os_release.name_id << " package version " << "for " << ps.system_name << info << "expected: " << ps.system_version << info << "installed: " << pp.installed_version << @@ -1412,4 +1425,78 @@ namespace bpkg } } } + + // Some background on creating Debian packages (for a bit more detailed + // overview see the Debian Packaging Tutorial). + // + // A binary Debian package (.deb) is an ar archive which itself contains a + // few tar archives normally compressed with gz or xz. So it's possible to + // create the package completely manually without using any of the Debian + // tools and while some implementations (for example, cargo-deb) do it this + // way, we are not going to go this route because it does not scale well to + // more complex packages which may require additional functionality, such as + // managing systemd files, and which is covered by the Debian tools (for an + // example of where this leads, see the partial debhelper re-implementation + // in cargo-deb). Another issues with this approach is that it's not + // amenable to customizations, at least not in a way familiar to Debian + // users. + // + // At the lowest level of the Debian tools for creating packages sits the + // dpkg-deb --build|-b command (also accessible as dpkg --build|-b). Given a + // directory with all the binary contents (including the package metadata, + // such as the control file, in the debian/ subdirectory) this command will + // pack everything up into a .deb file. While an improvement over the fully + // manual packaging, this approach has essentially the same drawbacks. In + // particular, this command generates a single package which means we will + // have to manually sort out things into -dev, -doc, etc. + // + // Next up the stack is dpkg-buildpackage. This tool expects the package to + // follow the Debian way, that is, to provide the debian/rules makefile with + // a number of required targets which it then invokes to build, install, and + // pack a package from source (and somewhere in this process it calls + // dpkg-deb --build). The dpkg-buildpackage(1) man page has an overview of + // all the steps that this command performs and it is the recommended, + // lower-level, way to build packages on Debian. + // + // At the top of the stack sits debuild which calls dpkg-buildpackage, then + // lintian and finally design (though signing can also be performed by + // dpkg-buildpackage). + // + // Based on this our plan is to use dpkg-buildpackage which brings us to the + // Debian way of packaging with debian/rules at its core. As it turns out, + // it can also be implemented in a number of alternative ways. So let's + // discuss those. + // + // As mentioned earlier, debian/rules is a makefile that is expected to + // provide a number of targets, such as build, install, etc. And + // theoretically these targets can be implemented completely manually. In + // practice, however, the Debian way is to use the debhelper(1) packaging + // helper tools. For example, there are helpers for stripping binaries, + // compressing man pages, fixing permissions, and managing systemd files. + // + // While debhelper tools definitely simplify debian/rules, there is often + // still a lot of boilerplate code. So second-level helpers are often used, + // with the dominant option being the dh(1) command sequencer (there is also + // CDBS but it appears to be mostly obsolete). + // + // Based on that our options appear to be classic debhelper and dh. Looking + // at the statistics, it's clear that the majority of packages (including + // fairly complex ones) tend to prefer dh and there is no reason for us to + // try to buck this trend. + // + // So, to sum up, the plan is to produce debian/rules that uses the dh + // command sequencer and then invoke dpkg-buildpackage to produce the binary + // package from that. While this approach is normally used to build things + // from source, it feels like we should be able to pretend that we are by, + // for example, overriding the install target to invoke the build system to + // install all the packages directly from their bpkg locations. + // + void system_package_manager_debian:: + generate (packages&&, + packages&&, + strings&&, + const dir_path&, + optional) + { + } } diff --git a/bpkg/system-package-manager-debian.hxx b/bpkg/system-package-manager-debian.hxx index 9fb93c7..e01b25d 100644 --- a/bpkg/system-package-manager-debian.hxx +++ b/bpkg/system-package-manager-debian.hxx @@ -14,7 +14,9 @@ namespace bpkg { // The system package manager implementation for Debian and alike (Ubuntu, - // etc) using the APT frontend. + // etc) using the apt frontend (specifically, apt-get and apt-cache) for + // consumption and the dpkg-buildpackage/debhelper/dh tooling for + // production. // // NOTE: the below description is also reproduced in the bpkg manual. // @@ -26,7 +28,7 @@ namespace bpkg // (e.g., libfoo1-common). All the packages except -dev are optional // and there is quite a bit of variability here. Here are a few examples: // - // libz3-4 libz3-dev + // libsqlite3-0 libsqlite3-dev // // libssl1.1 libssl-dev libssl-doc // libssl3 libssl-dev libssl-doc @@ -34,6 +36,9 @@ namespace bpkg // libcurl4 libcurl4-openssl-dev libcurl4-doc // libcurl3-gnutls libcurl4-gnutls-dev libcurl4-doc (yes, 3 and 4) // + // Note that while most library package names in Debian start with lib (per + // the policy), there are exceptions (e.g., zlib1g zlib1g-dev). + // // Based on that, it seems our best bet when trying to automatically map our // library package name to Debian package names is to go for the -dev // package first and figure out the shared library package from that based @@ -124,11 +129,44 @@ namespace bpkg virtual void pkg_install (const vector&) override; + virtual void + generate (packages&&, + packages&&, + strings&&, + const dir_path&, + optional) override; + public: - // Expects os_release::name_id to be "debian" or os_release::like_ids to + // Expect os_release::name_id to be "debian" or os_release::like_ids to // contain "debian". // - using system_package_manager::system_package_manager; + // @@ TODO: we currently don't handle non-host arch in consumption. + // + system_package_manager_debian (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional progress, + bool install, + bool fetch, + bool yes, + string sudo) + : system_package_manager (move (osr), + h, + a.empty () ? arch_from_target (h) : move (a), + progress, + install, + fetch, + yes, + move (sudo)) {} + + system_package_manager_debian (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional progress) + : system_package_manager (move (osr), + h, + a.empty () ? arch_from_target (h) : move (a), + progress) {} // Implementation details exposed for testing (see definitions for // documentation). @@ -158,6 +196,9 @@ namespace bpkg static string main_from_dev (const string&, const string&, const string&); + static string + arch_from_target (const target_triplet&); + // If simulate is not NULL, then instead of executing the actual apt-cache // and apt-get commands simulate their execution: (1) for apt-cache by // printing their command lines and reading the results from files diff --git a/bpkg/system-package-manager-debian.test.cxx b/bpkg/system-package-manager-debian.test.cxx index a033400..d719860 100644 --- a/bpkg/system-package-manager-debian.test.cxx +++ b/bpkg/system-package-manager-debian.test.cxx @@ -90,9 +90,10 @@ namespace bpkg system_package_manager_debian m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, false /* install */, false /* fetch */, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; @@ -117,9 +118,10 @@ namespace bpkg system_package_manager_debian m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, false /* install */, false /* fetch */, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; @@ -283,9 +285,10 @@ namespace bpkg system_package_manager_debian m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, install, fetch, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; diff --git a/bpkg/system-package-manager-fedora.cxx b/bpkg/system-package-manager-fedora.cxx index 7e6990e..b117eab 100644 --- a/bpkg/system-package-manager-fedora.cxx +++ b/bpkg/system-package-manager-fedora.cxx @@ -11,6 +11,17 @@ namespace bpkg { using package_status = system_package_status_fedora; + // Translate host CPU to Fedora package architecture. + // + string system_package_manager_fedora:: + arch_from_target (const target_triplet& h) + { + const string& c (h.cpu); + return + c == "i386" || c == "i486" || c == "i586" || c == "i686" ? "i686" : + c; + } + // Parse the fedora-name (or alike) value. // // Note that for now we treat all the packages from the non-main groups as @@ -413,9 +424,7 @@ namespace bpkg // Skip the package if its architecture differs from the host // architecture. // - // @@ TODO: arch translation. - // - if (a != host_.cpu && a != "noarch") + if (a != arch && a != "noarch") continue; p.resize (e); @@ -537,7 +546,7 @@ namespace bpkg vector> system_package_manager_fedora:: dnf_repoquery_requires (const string& name, const string& ver, - const string& arch) + const string& qarch) { assert (!name.empty () && !ver.empty () && !arch.empty ()); @@ -547,7 +556,7 @@ namespace bpkg // dependencies with different architecture (see the below example). It // feels sensible to just skip them. // - string spec (name + '-' + ver + '.' + arch); + string spec (name + '-' + ver + '.' + qarch); // The --quiet option makes sure we don't get 'Last metadata expiration // check: ' printed to stderr. It does not appear to affect @@ -593,7 +602,7 @@ namespace bpkg evars); else { - simulation::package k {name, ver, arch}; + simulation::package k {name, ver, qarch}; const path* f (nullptr); if (fetched_) @@ -685,7 +694,7 @@ namespace bpkg // Skip a potential self-dependency and dependencies of a different // architecture. // - if (p == name || (a != host_.cpu && a != "noarch")) + if (p == name || (a != arch && a != "noarch")) continue; r.emplace_back (move (p), move (v)); @@ -821,8 +830,7 @@ namespace bpkg if (verb >= 2) print_process (args); else if (verb == 1) - text << "updating " << os_release_.name_id - << " repositories metadata..."; + text << "updating " << os_release.name_id << " repositories metadata..."; process pr; if (!simulate_) @@ -846,7 +854,7 @@ namespace bpkg } if (verb == 1) - text << "updated " << os_release_.name_id << " repositories metadata"; + text << "updated " << os_release.name_id << " repositories metadata"; } catch (const process_error& e) { @@ -902,7 +910,7 @@ namespace bpkg if (verb >= 2) print_process (args); else if (verb == 1) - text << "installing " << os_release_.name_id << " packages..."; + text << "installing " << os_release.name_id << " packages..."; process pr; if (!simulate_) @@ -985,7 +993,7 @@ namespace bpkg } if (verb == 1) - text << "installed " << os_release_.name_id << " packages"; + text << "installed " << os_release.name_id << " packages"; } catch (const process_error& e) { @@ -1031,16 +1039,16 @@ namespace bpkg auto df = make_diag_frame ( [this, &pn] (diag_record& dr) { - dr << info << "while mapping " << pn << " to " - << os_release_.name_id << " package name"; + dr << info << "while mapping " << pn << " to " << os_release.name_id + << " package name"; }); strings ns; if (!aps->empty ()) ns = system_package_names (*aps, - os_release_.name_id, - os_release_.version_id, - os_release_.like_ids); + os_release.name_id, + os_release.version_id, + os_release.like_ids); if (ns.empty ()) { // Attempt to automatically translate our package name. Failed that we @@ -1134,17 +1142,17 @@ namespace bpkg // auto guess_main = [this, &pn] (package_status& s, const string& ver, - const string& arch) + const string& qarch) { vector> depends ( - dnf_repoquery_requires (s.devel, ver, arch)); + dnf_repoquery_requires (s.devel, ver, qarch)); s.main = main_from_devel (s.devel, ver, depends); if (s.main.empty ()) { diag_record dr (fail); - dr << "unable to guess main " << os_release_.name_id + dr << "unable to guess main " << os_release.name_id << " package for " << s.devel << ' ' << ver << info << "depends on"; @@ -1259,9 +1267,9 @@ namespace bpkg // found. Double-check Debian semantics. // fail << "unable to guess " << (devel ? "devel" : "main") - << ' ' << os_release_.name_id << " package for " << pn << + << ' ' << os_release.name_id << " package for " << pn << info << "neither " << name << " nor " << ps.fallback - << ' ' << os_release_.name_id << " package exists" << + << ' ' << os_release.name_id << " package exists" << info << "consider specifying explicit mapping in " << pn << " package manifest"; } @@ -1323,7 +1331,7 @@ namespace bpkg if (dr.empty ()) { - dr << fail << "multiple installed " << os_release_.name_id + dr << fail << "multiple installed " << os_release.name_id << " packages for " << pn << info << "candidate: " << r->main << " " << r->system_version; } @@ -1423,7 +1431,7 @@ namespace bpkg if (dr.empty ()) { dr << fail << "multiple partially installed " - << os_release_.name_id << " packages for " << pn; + << os_release.name_id << " packages for " << pn; dr << info << "candidate: " << r->main << " " << r->system_version << ", missing components:"; @@ -1459,7 +1467,7 @@ namespace bpkg if (dr.empty ()) { - dr << fail << "multiple available " << os_release_.name_id + dr << fail << "multiple available " << os_release.name_id << " packages for " << pn << info << "candidate: " << r->main << " " << r->system_version; } @@ -1488,9 +1496,9 @@ namespace bpkg if (!aps->empty ()) v = downstream_package_version (sv, *aps, - os_release_.name_id, - os_release_.version_id, - os_release_.like_ids); + os_release.name_id, + os_release.version_id, + os_release.like_ids); if (!v) { @@ -1507,10 +1515,10 @@ namespace bpkg } catch (const invalid_argument& e) { - fail << "unable to map " << os_release_.name_id << " package " + 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 " + info << os_release.name_id << " version is not a valid bpkg " << "version: " << e.what () << info << "consider specifying explicit mapping in " << pn << " package manifest"; @@ -1653,7 +1661,7 @@ namespace bpkg if (pi.installed_version != ps.system_version) { - fail << "unexpected " << os_release_.name_id << " package version " + fail << "unexpected " << os_release.name_id << " package version " << "for " << ps.system_name << info << "expected: " << ps.system_version << info << "installed: " << pi.installed_version << @@ -1662,4 +1670,13 @@ namespace bpkg } } } + + void system_package_manager_fedora:: + generate (packages&&, + packages&&, + strings&&, + const dir_path&, + optional) + { + } } diff --git a/bpkg/system-package-manager-fedora.hxx b/bpkg/system-package-manager-fedora.hxx index 16fe9a3..52d2a3f 100644 --- a/bpkg/system-package-manager-fedora.hxx +++ b/bpkg/system-package-manager-fedora.hxx @@ -205,10 +205,42 @@ namespace bpkg virtual void pkg_install (const vector&) override; + virtual void + generate (packages&&, + packages&&, + strings&&, + const dir_path&, + optional) override; + public: - // Expects os_release::name_id to be "fedora" or os_release::like_ids to + // Expect os_release::name_id to be "fedora" or os_release::like_ids to // contain "fedora". - using system_package_manager::system_package_manager; + // + system_package_manager_fedora (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional progress, + bool install, + bool fetch, + bool yes, + string sudo) + : system_package_manager (move (osr), + h, + a.empty () ? arch_from_target (h) : move (a), + progress, + install, + fetch, + yes, + move (sudo)) {} + + system_package_manager_fedora (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional progress) + : system_package_manager (move (osr), + h, + a.empty () ? arch_from_target (h) : move (a), + progress) {} // Implementation details exposed for testing (see definitions for // documentation). @@ -240,6 +272,9 @@ namespace bpkg const string&, const vector>&); + static string + arch_from_target (const target_triplet&); + // If simulate is not NULL, then instead of executing the actual dnf // commands simulate their execution: (1) for `dnf list` and `dnf // repoquery --requires` by printing their command lines and reading the diff --git a/bpkg/system-package-manager-fedora.test.cxx b/bpkg/system-package-manager-fedora.test.cxx index 2c5a976..8064d19 100644 --- a/bpkg/system-package-manager-fedora.test.cxx +++ b/bpkg/system-package-manager-fedora.test.cxx @@ -100,9 +100,10 @@ namespace bpkg system_package_manager_fedora m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, false /* install */, false /* fetch */, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; @@ -129,9 +130,10 @@ namespace bpkg system_package_manager_fedora m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, false /* install */, false /* fetch */, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; @@ -317,9 +319,10 @@ namespace bpkg system_package_manager_fedora m (move (osr), host_triplet, + "" /* arch */, + nullopt /* progress */, install, fetch, - nullopt /* progress */, false /* yes */, "sudo"); m.simulate_ = &s; diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx index 6a44b09..caf951f 100644 --- a/bpkg/system-package-manager.cxx +++ b/bpkg/system-package-manager.cxx @@ -27,70 +27,147 @@ namespace bpkg // vtable } + // Return true if the specified operating system is or like the specified + // id. + // + static inline bool + is_or_like (const os_release& os, const char* id) + { + return (os.name_id == id || + find_if (os.like_ids.begin (), os.like_ids.end (), + [id] (const string& n) + { + return n == id; + }) != os.like_ids.end ()); + } + unique_ptr - make_system_package_manager (const common_options& co, - const target_triplet& host, - bool install, - bool fetch, - bool yes, - const string& sudo, - const string& name) + make_consumption_system_package_manager (const common_options& co, + const target_triplet& host, + const string& name, + const string& arch, + bool install, + bool fetch, + bool yes, + const string& sudo) { + // Note: similar to make_consumption_system_package_manager() below. + optional progress (co.progress () ? true : co.no_progress () ? false : optional ()); unique_ptr r; - if (optional osr = host_os_release (host)) + if (optional oos = host_os_release (host)) { - auto is_or_like = [&osr] (const char* id) - { - return (osr->name_id == id || - find_if (osr->like_ids.begin (), osr->like_ids.end (), - [id] (const string& n) - { - return n == id; - }) != osr->like_ids.end ()); - }; + os_release& os (*oos); if (host.class_ == "linux") { - if (is_or_like ("debian") || - is_or_like ("ubuntu")) + if (is_or_like (os, "debian") || + is_or_like (os, "ubuntu")) { if (!name.empty () && name != "debian") fail << "unsupported package manager '" << name << "' for " - << osr->name_id << " host"; + << os.name_id << " host"; // If we recognized this as Debian-like in an ad hoc manner, then // add debian to like_ids. // - if (osr->name_id != "debian" && !is_or_like ("debian")) - osr->like_ids.push_back ("debian"); + if (os.name_id != "debian" && !is_or_like (os, "debian")) + os.like_ids.push_back ("debian"); r.reset (new system_package_manager_debian ( - move (*osr), host, install, fetch, progress, yes, sudo)); + move (os), host, arch, + progress, install, fetch, yes, sudo)); } - else if (is_or_like ("fedora") || - is_or_like ("rhel") || - is_or_like ("centos") || - is_or_like ("rocky") || - is_or_like ("almalinux")) + else if (is_or_like (os, "fedora") || + is_or_like (os, "rhel") || + is_or_like (os, "centos") || + is_or_like (os, "rocky") || + is_or_like (os, "almalinux")) { if (!name.empty () && name != "fedora") fail << "unsupported package manager '" << name << "' for " - << osr->name_id << " host"; + << os.name_id << " host"; // If we recognized this as Fedora-like in an ad hoc manner, then // add fedora to like_ids. // - if (osr->name_id != "fedora" && !is_or_like ("fedora")) - osr->like_ids.push_back ("fedora"); + if (os.name_id != "fedora" && !is_or_like (os, "fedora")) + os.like_ids.push_back ("fedora"); + + r.reset (new system_package_manager_fedora ( + move (os), host, arch, + progress, install, fetch, yes, sudo)); + } + // NOTE: remember to update the --sys-distribution pkg-build option + // documentation if adding support for another package manager. + } + } + + if (r == nullptr) + { + if (!name.empty ()) + fail << "unsupported package manager '" << name << "' for host " + << host; + } + + return r; + } + + unique_ptr + make_production_system_package_manager (const common_options& co, + const target_triplet& host, + const string& name, + const string& arch) + { + // Note: similar to make_production_system_package_manager() above. + + optional progress (co.progress () ? true : + co.no_progress () ? false : + optional ()); + + unique_ptr r; + + if (optional oos = host_os_release (host)) + { + os_release& os (*oos); + + if (host.class_ == "linux") + { + if (is_or_like (os, "debian") || + is_or_like (os, "ubuntu")) + { + if (!name.empty () && name != "debian") + fail << "unsupported package manager '" << name << "' for " + << os.name_id << " host"; + + if (os.name_id != "debian" && !is_or_like (os, "debian")) + os.like_ids.push_back ("debian"); + + r.reset (new system_package_manager_debian ( + move (os), host, arch, progress)); + } + else if (is_or_like (os, "fedora") || + is_or_like (os, "rhel") || + is_or_like (os, "centos") || + is_or_like (os, "rocky") || + is_or_like (os, "almalinux")) + { + if (!name.empty () && name != "fedora") + fail << "unsupported package manager '" << name << "' for " + << os.name_id << " host"; + + if (os.name_id != "fedora" && !is_or_like (os, "fedora")) + os.like_ids.push_back ("fedora"); r.reset (new system_package_manager_fedora ( - move (*osr), host, install, fetch, progress, yes, sudo)); + move (os), host, arch, progress)); } + // NOTE: remember to update the --distribution pkg-bindist option + // documentation if adding support for another package manager. } } diff --git a/bpkg/system-package-manager.hxx b/bpkg/system-package-manager.hxx index 9a9c443..63bf676 100644 --- a/bpkg/system-package-manager.hxx +++ b/bpkg/system-package-manager.hxx @@ -17,7 +17,7 @@ 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 + // (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. @@ -103,6 +103,13 @@ namespace bpkg 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: @@ -148,7 +155,32 @@ namespace bpkg virtual void pkg_install (const vector&) = 0; + // Generate a binary distribution package. + // + // @@ TODO: doc + // + // See the pkg-bindist(1) man page and the pkg_bindist() function + // implementation for background and details. + // + using packages = + vector, available_packages>>; + + enum class recursive_mode {auto_, full}; + + virtual void + generate (packages&& pkgs, + packages&& deps, + strings&& vars, + const dir_path& out, + optional) = 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 @@ -156,21 +188,37 @@ namespace bpkg // available version of the not yet installed or partially installed // packages. // - system_package_manager (os_release&& osr, - const target_triplet& host, + system_package_manager (bpkg::os_release&& osr, + const target_triplet& h, + string a, + optional progress, bool install, bool fetch, - optional progress, bool yes, string sudo) - : os_release_ (osr), - host_ (host), + : os_release (move (osr)), + host (h), + arch (move (a)), progress_ (progress), 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 (); @@ -235,8 +283,6 @@ namespace bpkg 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. @@ -248,8 +294,10 @@ namespace bpkg }; // 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: + // 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. @@ -258,13 +306,20 @@ namespace bpkg // 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); + 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); + + unique_ptr + make_production_system_package_manager (const common_options&, + const target_triplet&, + const string& name, + const string& arch); } #endif // BPKG_SYSTEM_PACKAGE_MANAGER_HXX diff --git a/bpkg/system-repository.hxx b/bpkg/system-repository.hxx index 31e14d1..6fc04f9 100644 --- a/bpkg/system-repository.hxx +++ b/bpkg/system-repository.hxx @@ -18,7 +18,7 @@ namespace bpkg { // A map of discovered system package versions. The information can be // authoritative (i.e., it was provided by the user or auto-discovered - // on this run) or non-authoritative (i.e., comes from selected_packages + // on this run) or non-authoritative (i.e., comes from selected packages // that are present in the database; in a sence it was authoritative but // on some previous run. // -- cgit v1.1