From 5d2c51cbcfa8e75ab972b5bf5864d7a5880c7156 Mon Sep 17 00:00:00 2001 From: Boris Kolpackov Date: Mon, 13 Mar 2023 09:44:36 +0200 Subject: Move os-release facility from bpkg, add support for Mac OS, *BSD, Windows --- libbutl/host-os-release.cxx | 323 +++++++++++++++++++++++++++++++++++++++ libbutl/host-os-release.hxx | 86 +++++++++++ libbutl/target-triplet.hxx | 2 + tests/host-os-release/buildfile | 6 + tests/host-os-release/driver.cxx | 58 +++++++ tests/host-os-release/testscript | 223 +++++++++++++++++++++++++++ 6 files changed, 698 insertions(+) create mode 100644 libbutl/host-os-release.cxx create mode 100644 libbutl/host-os-release.hxx create mode 100644 tests/host-os-release/buildfile create mode 100644 tests/host-os-release/driver.cxx create mode 100644 tests/host-os-release/testscript diff --git a/libbutl/host-os-release.cxx b/libbutl/host-os-release.cxx new file mode 100644 index 0000000..f13f62c --- /dev/null +++ b/libbutl/host-os-release.cxx @@ -0,0 +1,323 @@ +// file : libbutl/host-os-release.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include +#include // runtime_error + +#include +#include +#include +#include +#include +#include // file_exists() +#include // parse_quoted() + +#ifdef _WIN32 +# include +#endif + +using namespace std; + +namespace butl +{ + // Note: exported for access from the test. + // + LIBBUTL_SYMEXPORT 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. + // + auto exists = [] (const path& f) + { + try + { + return file_exists (f); + } + catch (const system_error& e) + { + ostringstream os; + os << "unable to stat path " << f << ": " << e; + throw runtime_error (os.str ()); + } + }; + + 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) + { + ostringstream os; + os << "invalid " << n << " value in " << f << ':' << ln << ": " + << e; + throw runtime_error (os.str ()); + } + } + + ifs.close (); + } + catch (const ios::failure& e) + { + ostringstream os; + os << "unable to read from " << f << ": " << e; + throw runtime_error (os.str ()); + } + } + + // Assign fallback values. + // + if (r.name_id.empty ()) r.name_id = "linux"; + if (r.name.empty ()) r.name = "Linux"; + + return r; + } + + static os_release + host_os_release_macos () + { + // Run sw_vers -productVersion to get Mac OS version. + // + try + { + process pr; + try + { + fdpipe pipe (fdopen_pipe ()); + + pr = process_start (0, pipe, 2, "sw_vers", "-productVersion"); + + pipe.out.close (); + ifdstream is (move (pipe.in), fdstream_mode::skip, ifdstream::badbit); + + // The output should be one line containing the version. + // + optional v; + for (string l; !eof (getline (is, l)); ) + { + if (l.empty () || v) + { + v = nullopt; + break; + } + + v = move (l); + } + + is.close (); // Detect errors. + + if (pr.wait ()) + { + if (!v) + throw runtime_error ("unexpected sw_vers -productVersion output"); + + return os_release {"macos", {}, move (*v), "", "Mac OS", "", ""}; + } + + } + catch (const ios::failure& e) + { + if (pr.wait ()) + { + ostringstream os; + os << "error reading sw_vers output: " << e; + throw runtime_error (os.str ()); + } + + // Fall through. + } + + // We should only get here if the child exited with an error status. + // + assert (!pr.wait ()); + throw runtime_error ("process sw_vers exited with non-zero code"); + } + catch (const process_error& e) + { + ostringstream os; + os << "unable to execute sw_vers: " << e; + throw runtime_error (os.str ()); + } + } + + static os_release + host_os_release_windows () + { +#ifdef _WIN32 + // The straightforward way to get the version would be the GetVersionEx() + // Win32 function. However, if the application is built with a certain + // assembly manifest, this function will return the version the + // application was built for rather than what's actually running. + // + // The other plausible options are to call the `ver` program and parse it + // output (of questionable regularity) or to call RtlGetVersion(). The + // latter combined with GetProcAddress() seems to be a widely-used + // approach, so we are going with that (seeing that we employ a similar + // technique in quite a few places). + // + HMODULE nh (GetModuleHandle ("ntdll.dll")); + if (nh == nullptr) + throw runtime_error ("unable to get handle to ntdll.dll"); + + using RtlGetVersion = LONG /*NTSTATUS*/ (WINAPI*)(PRTL_OSVERSIONINFOW); + + RtlGetVersion gv ( + function_cast ( + GetProcAddress (nh, "RtlGetVersion"))); + + // RtlGetVersion() is available from Windows 2000 which is way before + // anything we might possibly care about (e.g., XP or 7). + // + if (gv == nullptr) + throw runtime_error ("unable to get address of RtlGetVersion()"); + + RTL_OSVERSIONINFOW vi; + vi.dwOSVersionInfoSize = sizeof (vi); + gv (&vi); // Always succeeds, according to documentation. + + // Ok, the real mess starts here. Here is how the commonly known Windows + // versions correspond to the major/minor/build numbers and how we will + // map them (note that there are also Server versions in the mix; see the + // OSVERSIONINFOEXW struct documentation for the complete picture): + // + // major minor build mapped + // Windows 11 10 0 >=22000 11 + // Windows 10 10 0 <22000 10 + // Windows 8.1 6 3 8.1 + // Windows 8 6 2 8 + // Windows 7 6 1 7 + // Windows Vista 6 0 6 + // Windows XP Pro/64-bit 5 2 5.2 + // Windows XP 5 1 5.1 + // Windows 2000 5 0 5 + // + // Based on this it's probably not wise to try to map any future versions + // automatically. + // + string v; + if (vi.dwMajorVersion == 10 && vi.dwMinorVersion == 0) + { + v = vi.dwBuildNumber >= 22000 ? "11" : "10"; + } + else if (vi.dwMajorVersion == 6 && vi.dwMinorVersion == 3) v = "8.1"; + else if (vi.dwMajorVersion == 6 && vi.dwMinorVersion == 2) v = "8"; + else if (vi.dwMajorVersion == 6 && vi.dwMinorVersion == 1) v = "7"; + else if (vi.dwMajorVersion == 6 && vi.dwMinorVersion == 0) v = "6"; + else if (vi.dwMajorVersion == 5 && vi.dwMinorVersion == 2) v = "5.2"; + else if (vi.dwMajorVersion == 5 && vi.dwMinorVersion == 1) v = "5.1"; + else if (vi.dwMajorVersion == 5 && vi.dwMinorVersion == 0) v = "5"; + else throw ("unknown windows version " + + std::to_string (vi.dwMajorVersion) + '.' + + std::to_string (vi.dwMinorVersion) + '.' + + std::to_string (vi.dwBuildNumber)); + + return os_release {"windows", {}, move (v), "", "Windows", "", ""}; +#else + throw runtime_error ("unexpected host operating system"); +#endif + } + + optional + host_os_release (const target_triplet& h) + { + const string& c (h.class_); + const string& s (h.system); + + if (c == "linux") + return host_os_release_linux (); + + if (c == "macos") + return host_os_release_macos (); + + if (c == "windows") + return host_os_release_windows (); + + if (c == "bsd") + { + // @@ TODO: ideally we would want to run uname and obtain the actual + // version we are runnig on rather than what we've been built for. + // (Think also how this will affect tests). + // + if (s == "freebsd") + return os_release {"freebsd", {}, h.version, "", "FreeBSD", "", ""}; + + if (s == "netbsd") + return os_release {"netbsd", {}, h.version, "", "NetBSD", "", ""}; + + if (s == "openbsd") + return os_release {"openbsd", {}, h.version, "", "OpenBSD", "", ""}; + + // Assume some other BSD. + // + return os_release {s, {}, h.version, "", s, "", ""}; + } + + return nullopt; + } +} diff --git a/libbutl/host-os-release.hxx b/libbutl/host-os-release.hxx new file mode 100644 index 0000000..058afdc --- /dev/null +++ b/libbutl/host-os-release.hxx @@ -0,0 +1,86 @@ +// file : libbutl/host-os-release.hxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#pragma once + +#include +#include + +#include +#include + +#include + +namespace butl +{ + // 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", ""} + // + // {"macos", {}, "12.5", "", + // "Mac OS", "", ""} + // + // {"freebsd", {}, "13.1", "", + // "FreeBSD", "", ""} + // + // {"windows", {}, "10", "", + // "Windows", "", ""} + // + // Note that for Mac OS, the version is the Mac OS version (as printed by + // sw_vers) rather than Darwin version (as printed by uname). + // + // For Windows we currently do not distinguish the Server edition and the + // version mapping is as follows: + // + // Windows 11 11 + // Windows 10 10 + // Windows 8.1 8.1 + // Windows 8 8 + // Windows 7 7 + // Windows Vista 6 + // Windows XP Pro/64-bit 5.2 + // Windows XP 5.1 + // Windows 2000 5 + // + // 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 + { + std::string name_id; // ID + std::vector like_ids; // ID_LIKE + std::string version_id; // VERSION_ID + std::string variant_id; // VARIANT_ID + + std::string name; // NAME + std::string version_codename; // VERSION_CODENAME + std::string variant; // VARIANT + }; + + // Return the release information for the specified host or nullopt if the + // specific host is unknown/unsupported. Throw std::runtime_error if + // anything goes wrong. + // + // Note that "host" here implies that we may be running programs, reading + // files, examining environment variables, etc., of the machine we are + // running on. + // + LIBBUTL_SYMEXPORT optional + host_os_release (const target_triplet& host); +} diff --git a/libbutl/target-triplet.hxx b/libbutl/target-triplet.hxx index da29907..e03bdaf 100644 --- a/libbutl/target-triplet.hxx +++ b/libbutl/target-triplet.hxx @@ -100,6 +100,8 @@ namespace butl // windows *-*-win32-* | *-*-windows-* | *-*-mingw32 // ios *-apple-ios* // + // NOTE: see also os_release if adding anything new here. + // // References: // // 1. The libtool repository contains the PLATFORM file that lists many known diff --git a/tests/host-os-release/buildfile b/tests/host-os-release/buildfile new file mode 100644 index 0000000..cd277ff --- /dev/null +++ b/tests/host-os-release/buildfile @@ -0,0 +1,6 @@ +# file : tests/host-os-release/buildfile +# license : MIT; see accompanying LICENSE file + +import libs = libbutl%lib{butl} + +exe{driver}: {hxx cxx}{*} $libs testscript diff --git a/tests/host-os-release/driver.cxx b/tests/host-os-release/driver.cxx new file mode 100644 index 0000000..249cbff --- /dev/null +++ b/tests/host-os-release/driver.cxx @@ -0,0 +1,58 @@ +// file : tests/host-os-release/driver.cxx -*- C++ -*- +// license : MIT; see accompanying LICENSE file + +#include + +#include + +namespace butl +{ + LIBBUTL_SYMEXPORT os_release + host_os_release_linux (path f = {}); +} + +#include + +#undef NDEBUG +#include + +using namespace std; +using namespace butl; + +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 (argc == 2); + if (optional o = host_os_release (host)) + r = move (*o); + else + { + cerr << "unrecognized host os " << host.string () << endl; + return 1; + } + } + + 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; +} diff --git a/tests/host-os-release/testscript b/tests/host-os-release/testscript new file mode 100644 index 0000000..a18aa74 --- /dev/null +++ b/tests/host-os-release/testscript @@ -0,0 +1,223 @@ +# file : tests/host-os-release/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 + +: macos +: +if ($build.host.class == 'macos') +{ + $* $build.host >>~/EOO/ + macos + + /[0-9]+(\.[0-9]+(\.[0-9]+)?)?/ + + Mac OS + + + EOO +} + +: freebsd +: +if ($build.host.system == 'freebsd') +{ + $* $build.host >>~/EOO/ + freebsd + + /[0-9]+\.[0-9]+/ + + FreeBSD + + + EOO +} + +: windows +: +if ($build.host.system == 'windows') +{ + $* $build.host >>~/EOO/ + windows + + /[0-9]+(\.[0-9]+)?/ + + Windows + + + EOO +} -- cgit v1.1