From 8e20c1ed163be60cb576b5ad8f5d27b6320154a6 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Mon, 16 Jan 2023 13:53:43 +0200 Subject: Add os_release facility --- bpkg/host-os-release.cxx | 121 ++++++++++++++++++++++++ bpkg/host-os-release.hxx | 57 +++++++++++ bpkg/host-os-release.test.cxx | 55 +++++++++++ bpkg/host-os-release.test.testscript | 178 +++++++++++++++++++++++++++++++++++ bpkg/package-query.hxx | 2 + bpkg/pkg-build.cxx | 3 + bpkg/system-package-manager.cxx | 25 ++++- bpkg/system-package-manager.hxx | 73 +++++++++++++- 8 files changed, 507 insertions(+), 7 deletions(-) create mode 100644 bpkg/host-os-release.cxx create mode 100644 bpkg/host-os-release.hxx create mode 100644 bpkg/host-os-release.test.cxx create mode 100644 bpkg/host-os-release.test.testscript diff --git a/bpkg/host-os-release.cxx b/bpkg/host-os-release.cxx new file mode 100644 index 0000000..1a5a980 --- /dev/null +++ b/bpkg/host-os-release.cxx @@ -0,0 +1,121 @@ +// file : bpkg/host-os-release.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include // parse_quoted() + +#include + +using namespace butl; + +namespace bpkg +{ + // Note: not static for access from the unit test. + // + os_release + host_os_release_linux (path f = {}) + { + os_release r; + + // According to os-release(5), we should use /etc/os-release and fallback + // to /usr/lib/os-release if the former does not exist. It also lists the + // fallback values for individual variables, in case some are not present. + // + if (!f.empty () + ? exists (f) + : (exists (f = path ("/etc/os-release")) || + exists (f = path ("/usr/lib/os-release")))) + { + try + { + ifdstream ifs (f, ifdstream::badbit); + + string l; + for (uint64_t ln (1); !eof (getline (ifs, l)); ++ln) + { + trim (l); + + // Skip blanks lines and comments. + // + if (l.empty () || l[0] == '#') + continue; + + // The variable assignments are in the "shell style" and so can be + // quoted/escaped. For now we only handle quoting, which is what all + // the instances seen in the wild seems to use. + // + size_t p (l.find ('=')); + if (p == string::npos) + continue; + + string n (l, 0, p); + l.erase (0, p + 1); + + using string_parser::parse_quoted; + using string_parser::invalid_string; + + try + { + if (n == "ID_LIKE") + { + r.like_ids.clear (); + + vector vs (parse_quoted (l, true /* unquote */)); + for (const string& v: vs) + { + for (size_t b (0), e (0); next_word (v, b, e); ) + { + r.like_ids.push_back (string (v, b, e - b)); + } + } + } + else if (string* p = (n == "ID" ? &r.name_id : + n == "VERSION_ID" ? &r.version_id : + n == "VARIANT_ID" ? &r.variant_id : + n == "NAME" ? &r.name : + n == "VERSION_CODENAME" ? &r.version_codename : + n == "VARIANT" ? &r.variant : + nullptr)) + { + vector vs (parse_quoted (l, true /* unquote */)); + switch (vs.size ()) + { + case 0: *p = ""; break; + case 1: *p = move (vs.front ()); break; + default: throw invalid_string (0, "multiple values"); + } + } + } + catch (const invalid_string& e) + { + location loc (move (f).string (), ln); + fail (loc) << "invalid " << n << " value: " << e; + } + } + + ifs.close (); + } + catch (const io_error& e) + { + fail << "unable to read from " << f << ": " << e; + } + } + + // Assign fallback values. + // + if (r.name_id.empty ()) r.name_id = "linux"; + if (r.name.empty ()) r.name = "Linux"; + + return r; + } + + optional + host_os_release (const target_triplet& host) + { + if (host.class_ == "linux") + return host_os_release_linux (); + else + return nullopt; + } +} diff --git a/bpkg/host-os-release.hxx b/bpkg/host-os-release.hxx new file mode 100644 index 0000000..c2d6672 --- /dev/null +++ b/bpkg/host-os-release.hxx @@ -0,0 +1,57 @@ +// file : bpkg/host-os-release.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#ifndef BPKG_HOST_OS_RELEASE_HXX +#define BPKG_HOST_OS_RELEASE_HXX + +#include +#include + +namespace bpkg +{ + // Information extracted from /etc/os-release on Linux. See os-release(5) + // for background. For other platforms we derive the equivalent information + // from other sources. Some examples: + // + // {"debian", {}, "10", "", + // "Debian GNU/Linux", "buster", ""} + // + // {"fedora", {}, "35", "workstation", + // "Fedora Linux", "", "Workstation Edition"} + // + // {"ubuntu", {"debian"}, "20.04", "", + // "Ubuntu", "focal", ""} + // + // {"windows", {}, "10", "", + // "Windows", "", ""} + // + // Note that version_id may be empty, for example, on Debian testing: + // + // {"debian", {}, "", "", + // "Debian GNU/Linux", "", ""} + // + // Note also that we don't extract PRETTY_NAME because its content is + // unpredictable. For example, it may include variant, as in "Fedora Linux + // 35 (Workstation Edition)". Instead, construct it from the individual + // components as appropriate, normally "$name $version ($version_codename)". + // + struct os_release + { + string name_id; // ID + vector like_ids; // ID_LIKE + string version_id; // VERSION_ID + string variant_id; // VARIANT_ID + + string name; // NAME + string version_codename; // VERSION_CODENAME + string variant; // VARIANT + }; + + // Return the release information for the specified host or nullopt if + // the specific host is unknown/unsupported. + // + optional + host_os_release (const target_triplet& host); +} + +#endif // BPKG_HOST_OS_RELEASE_HXX diff --git a/bpkg/host-os-release.test.cxx b/bpkg/host-os-release.test.cxx new file mode 100644 index 0000000..e65c3ca --- /dev/null +++ b/bpkg/host-os-release.test.cxx @@ -0,0 +1,55 @@ +// file : bpkg/host-os-release.test.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include + +#include + +#undef NDEBUG +#include + +using namespace std; + +namespace bpkg +{ + extern os_release + host_os_release_linux (path f = {}); + + int + main (int argc, char* argv[]) + { + assert (argc >= 2); // + + target_triplet host (argv[1]); + + os_release r; + if (host.class_ == "linux") + { + assert (argc == 3); // + r = host_os_release_linux (path (argv[2])); + } + else + assert (false); + + cout << r.name_id << '\n'; + for (auto b (r.like_ids.begin ()), i (b); i != r.like_ids.end (); ++i) + cout << (i != b ? "|" : "") << *i; + cout << '\n' + << r.version_id << '\n' + << r.variant_id << '\n' + << r.name << '\n' + << r.version_codename << '\n' + << r.variant << '\n'; + + return 0; + } +} + +int +main (int argc, char* argv[]) +{ + return bpkg::main (argc, argv); +} diff --git a/bpkg/host-os-release.test.testscript b/bpkg/host-os-release.test.testscript new file mode 100644 index 0000000..b32c477 --- /dev/null +++ b/bpkg/host-os-release.test.testscript @@ -0,0 +1,178 @@ +# file : bpkg/host-os-release.test.testscript +# license : MIT; see accompanying LICENSE file + +: linux +: +$* x86_64-linux-gnu os-release >>EOO + linux + + + + Linux + + + EOO + +: debian-10 +: +cat <=os-release; + PRETTY_NAME="Debian GNU/Linux 10 (buster)" + NAME="Debian GNU/Linux" + VERSION_ID="10" + VERSION="10 (buster)" + VERSION_CODENAME=buster + ID=debian + HOME_URL="https://www.debian.org/" + SUPPORT_URL="https://www.debian.org/support" + BUG_REPORT_URL="https://bugs.debian.org/" + EOI +$* x86_64-linux-gnu os-release >>EOO + debian + + 10 + + Debian GNU/Linux + buster + + EOO + +: debian-testing +: +cat <=os-release; + PRETTY_NAME="Debian GNU/Linux bookworm/sid" + NAME="Debian GNU/Linux" + ID=debian + HOME_URL="https://www.debian.org/" + SUPPORT_URL="https://www.debian.org/support" + BUG_REPORT_URL="https://bugs.debian.org/" + EOI +$* x86_64-linux-gnu os-release >>EOO + debian + + + + Debian GNU/Linux + + + EOO + +: ubuntu-20.04 +: +cat <=os-release; + NAME="Ubuntu" + VERSION="20.04.1 LTS (Focal Fossa)" + ID=ubuntu + ID_LIKE=debian + PRETTY_NAME="Ubuntu 20.04.1 LTS" + VERSION_ID="20.04" + HOME_URL="https://www.ubuntu.com/" + SUPPORT_URL="https://help.ubuntu.com/" + BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" + PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" + VERSION_CODENAME=focal + UBUNTU_CODENAME=focal + EOI +$* x86_64-linux-gnu os-release >>EOO + ubuntu + debian + 20.04 + + Ubuntu + focal + + EOO + +: fedora-35 +: +cat <=os-release; + NAME="Fedora Linux" + VERSION="35 (Workstation Edition)" + ID=fedora + VERSION_ID=35 + VERSION_CODENAME="" + PLATFORM_ID="platform:f35" + PRETTY_NAME="Fedora Linux 35 (Workstation Edition)" + ANSI_COLOR="0;38;2;60;110;180" + LOGO=fedora-logo-icon + CPE_NAME="cpe:/o:fedoraproject:fedora:35" + HOME_URL="https://fedoraproject.org/" + DOCUMENTATION_URL="https://docs.fedoraproject.org/en-US/fedora/f35/system-administrators-guide/" + SUPPORT_URL="https://ask.fedoraproject.org/" + BUG_REPORT_URL="https://bugzilla.redhat.com/" + REDHAT_BUGZILLA_PRODUCT="Fedora" + REDHAT_BUGZILLA_PRODUCT_VERSION=35 + REDHAT_SUPPORT_PRODUCT="Fedora" + REDHAT_SUPPORT_PRODUCT_VERSION=35 + PRIVACY_POLICY_URL="https://fedoraproject.org/wiki/Legal:PrivacyPolicy" + VARIANT="Workstation Edition" + VARIANT_ID=workstation + EOI +$* x86_64-linux-gnu os-release >>EOO + fedora + + 35 + workstation + Fedora Linux + + Workstation Edition + EOO + +: rhel-8.2 +: +cat <=os-release; + NAME="Red Hat Enterprise Linux" + VERSION="8.2 (Ootpa)" + ID="rhel" + ID_LIKE="fedora" + VERSION_ID="8.2" + PLATFORM_ID="platform:el8" + PRETTY_NAME="Red Hat Enterprise Linux 8.2 (Ootpa)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:redhat:enterprise_linux:8.2:GA" + HOME_URL="https://www.redhat.com/" + BUG_REPORT_URL="https://bugzilla.redhat.com/" + + REDHAT_BUGZILLA_PRODUCT="Red Hat Enterprise Linux 8" + REDHAT_BUGZILLA_PRODUCT_VERSION=8.2 + REDHAT_SUPPORT_PRODUCT="Red Hat Enterprise Linux" + REDHAT_SUPPORT_PRODUCT_VERSION="8.2" + EOI +$* x86_64-linux-gnu os-release >>EOO + rhel + fedora + 8.2 + + Red Hat Enterprise Linux + + + EOO + +: centos-8 +: +cat <=os-release; + NAME="CentOS Linux" + VERSION="8 (Core)" + ID="centos" + ID_LIKE="rhel fedora" + VERSION_ID="8" + PLATFORM_ID="platform:el8" + PRETTY_NAME="CentOS Linux 8 (Core)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:centos:centos:8" + HOME_URL="https://www.centos.org/" + BUG_REPORT_URL="https://bugs.centos.org/" + + CENTOS_MANTISBT_PROJECT="CentOS-8" + CENTOS_MANTISBT_PROJECT_VERSION="8" + REDHAT_SUPPORT_PRODUCT="centos" + REDHAT_SUPPORT_PRODUCT_VERSION="8" + EOI +$* x86_64-linux-gnu os-release >>EOO + centos + rhel|fedora + 8 + + CentOS Linux + + + EOO diff --git a/bpkg/package-query.hxx b/bpkg/package-query.hxx index 1919058..1fbedd7 100644 --- a/bpkg/package-query.hxx +++ b/bpkg/package-query.hxx @@ -182,6 +182,8 @@ namespace bpkg // Note that we return (loaded) lazy_shared_ptr in order to also convey // the database to which it belongs. // + // @@ Add available_packages typedef and use everywhere. + // vector, lazy_shared_ptr>> find_available_all (const linked_databases&, diff --git a/bpkg/pkg-build.cxx b/bpkg/pkg-build.cxx index b35fe0c..14b0852 100644 --- a/bpkg/pkg-build.cxx +++ b/bpkg/pkg-build.cxx @@ -1708,6 +1708,9 @@ namespace bpkg // //system_package_manager& spm (**sys_pkg_mgr); + // @@ TODO: if find_available_all() returns empty list, then this is + // an error (no source/stub for the package). Issue diag similar + // to other cases where we suggest specifing /*. //@@ TODO: if we extracted a version, then we need to add an entry // to the imaginary stubs (probably checking for duplicated), just diff --git a/bpkg/system-package-manager.cxx b/bpkg/system-package-manager.cxx index 1503c32..aa9bed8 100644 --- a/bpkg/system-package-manager.cxx +++ b/bpkg/system-package-manager.cxx @@ -15,14 +15,33 @@ namespace bpkg unique_ptr make_system_package_manager (const target_triplet& host, - const string& type) + const string& name) { unique_ptr r; + if (optional osr = host_os_release (host)) + { + if (host.class_ == "linux") + { + if (osr->name_id == "debian" || + osr->name_id == "ubuntu" || + find_if (osr->like_ids.begin (), osr->like_ids.end (), + [] (const string& n) + { + return n == "debian" || n == "ubuntu"; + }) != osr->like_ids.end ()) + { + // @@ TODO: verify name if specified. + + //r.reset (new system_package_manager_debian (move (*osr))); + } + } + } + if (r == nullptr) { - if (!type.empty ()) - fail << "unsupported package manager type '" << type << "' for host " + if (!name.empty ()) + fail << "unsupported package manager '" << name << "' for host " << host; } diff --git a/bpkg/system-package-manager.hxx b/bpkg/system-package-manager.hxx index 122b181..4f01d1d 100644 --- a/bpkg/system-package-manager.hxx +++ b/bpkg/system-package-manager.hxx @@ -4,12 +4,14 @@ #ifndef BPKG_SYSTEM_PACKAGE_MANAGER_HXX #define BPKG_SYSTEM_PACKAGE_MANAGER_HXX -//#include // version -//#include +#include // version +#include #include #include +#include + namespace bpkg { // The system package manager interface. Used by both pkg-build (to query @@ -18,17 +20,80 @@ namespace bpkg class system_package_manager { public: + struct package_status + { + bpkg::version version; + + // The system package can be either "available already installed" + // or "available not yet installed". + // + // If the package is partially installed (for example, libfoo but not + // libfoo-dev is installed), then installed should be false (and perhaps + // only a single available version should be returned). + // + bool installed; + + string system_name; + string system_version; + }; + + // @@ We actually need to fetch is some are not installed to get their + // versions. We can do it as part of the call, no? + + public: virtual ~system_package_manager (); + + explicit + system_package_manager (os_release&& osr) + : os_release_ (osr) {} + + protected: + // Given the available packages (as returned by find_available_all()) + // return the list of system package names. + // + // The name_id, version_id, and like_id are the values from the os_release + // struct (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 of less than the specified version_id + // (include the value with the absent ). In a sense, absent + // can be treated as 0 semver-like versions. + // + // If no value is found and like_id is not empty, then repeat the above + // process for like_id instead of name_id and 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 the like_id's + // version and try that). + // + static vector + system_package_names (/*const available_packages&,*/ + const string& name_id, + const string& version_id, + const string& like_id); + + protected: + os_release os_release_; }; // Create a package manager instance corresponding to the specified host - // target and optional manager type. If type is empty, return NULL if there + // target and optional manager name. If name is empty, return NULL if there // is no support for this platform. // + // @@ TODO: need to assign names. Ideas: + // + // dpkg-apt, rpm-dnf + // deb, rpm + // debian, fedora (i.e., follow /etc/os-release ID_LIKE lead) + // unique_ptr make_system_package_manager (const target_triplet&, - const string& type); + const string& name); } #endif // BPKG_SYSTEM_PACKAGE_MANAGER_HXX -- cgit v1.1