瀏覽代碼

cmake_host_system_information: Can read `/etc/os-release` file

Alex Turbov 4 年之前
父節點
當前提交
1e65e4a6e5

+ 45 - 0
Help/command/cmake_host_system_information.rst

@@ -132,7 +132,52 @@ queried.  The list of queried values is stored in ``<variable>``.
 
   See :variable:`CMAKE_HOST_SYSTEM_PROCESSOR`
 
+For Linux distributions additional ``<key>`` values are available to get operating
+system identification as described in the `man 5 os-release`_.
+
+``DISTRIB_INFO``
+  .. versionadded:: 3.22
+
+  Read :file:`/etc/os-release` file and define the given ``<variable>``
+  into a list of read variables
+
+``DISTRIB_<name>``
+  .. versionadded:: 3.22
+
+  Get the ``<name>`` variable if it exists in the :file:`/etc/os-release` file
+
+  Example:
+
+  .. code-block:: cmake
+
+      cmake_host_system_information(RESULT PRETTY_NAME QUERY DISTRIB_PRETTY_NAME)
+      message(STATUS "${PRETTY_NAME}")
+
+      cmake_host_system_information(RESULT DISTRO QUERY DISTRIB_INFO)
+
+      foreach(VAR IN LISTS DISTRO)
+        message(STATUS "${VAR}=`${${VAR}}`")
+      endforeach()
+
+
+  Output::
+
+    -- Ubuntu 20.04.2 LTS
+    -- DISTRO_BUG_REPORT_URL=`https://bugs.launchpad.net/ubuntu/`
+    -- DISTRO_HOME_URL=`https://www.ubuntu.com/`
+    -- DISTRO_ID=`ubuntu`
+    -- DISTRO_ID_LIKE=`debian`
+    -- DISTRO_NAME=`Ubuntu`
+    -- DISTRO_PRETTY_NAME=`Ubuntu 20.04.2 LTS`
+    -- DISTRO_PRIVACY_POLICY_URL=`https://www.ubuntu.com/legal/terms-and-policies/privacy-policy`
+    -- DISTRO_SUPPORT_URL=`https://help.ubuntu.com/`
+    -- DISTRO_UBUNTU_CODENAME=`focal`
+    -- DISTRO_VERSION=`20.04.2 LTS (Focal Fossa)`
+    -- DISTRO_VERSION_CODENAME=`focal`
+    -- DISTRO_VERSION_ID=`20.04`
 
 .. rubric:: Footnotes
 
 .. [#mebibytes] One MiB (mebibyte) is equal to 1024x1024 bytes.
+
+.. _man 5 os-release: https://www.freedesktop.org/software/systemd/man/os-release.html

+ 6 - 0
Help/release/dev/os-release.rst

@@ -0,0 +1,6 @@
+os-release
+----------
+
+* The :command:`cmake_host_system_information` command query operating system
+  identification `variables <https://www.freedesktop.org/software/systemd/man/os-release.html>`_
+  from the :file:`/etc/os-release` file.

+ 191 - 6
Source/cmCMakeHostSystemInformationCommand.cxx

@@ -2,28 +2,37 @@
    file Copyright.txt or https://cmake.org/licensing for details.  */
 #include "cmCMakeHostSystemInformationCommand.h"
 
-#include <cstddef>
+#include <cassert>
+#include <cctype>
+#include <initializer_list>
+#include <map>
+#include <string>
+#include <type_traits>
+#include <utility>
 
 #include <cm/optional>
 #include <cm/string_view>
 #include <cmext/string_view>
 
+#include "cmsys/FStream.hxx"
 #include "cmsys/SystemInformation.hxx"
 
 #include "cmExecutionStatus.h"
 #include "cmMakefile.h"
+#include "cmStringAlgorithms.h"
+#include "cmSystemTools.h"
 
 #ifdef _WIN32
 #  include "cmAlgorithms.h"
 #  include "cmGlobalGenerator.h"
 #  include "cmGlobalVisualStudioVersionedGenerator.h"
-#  include "cmStringAlgorithms.h"
-#  include "cmSystemTools.h"
 #  include "cmVSSetupHelper.h"
 #  define HAVE_VS_SETUP_HELPER
 #endif
 
 namespace {
+std::string const DELIM[2] = { {}, ";" };
+
 // BEGIN Private functions
 std::string ValueToString(std::size_t const value)
 {
@@ -138,6 +147,177 @@ cm::optional<std::string> GetValue(cmsys::SystemInformation& info,
   return {};
 }
 
+#ifdef __linux__
+cm::optional<std::pair<std::string, std::string>> ParseOSReleaseLine(
+  std::string const& line)
+{
+  std::string key;
+  std::string value;
+
+  char prev = 0;
+  enum ParserState
+  {
+    PARSE_KEY_1ST,
+    PARSE_KEY,
+    FOUND_EQ,
+    PARSE_SINGLE_QUOTE_VALUE,
+    PARSE_DBL_QUOTE_VALUE,
+    PARSE_VALUE,
+    IGNORE_REST
+  } state = PARSE_KEY_1ST;
+
+  for (auto ch : line) {
+    switch (state) {
+      case PARSE_KEY_1ST:
+        if (std::isalpha(ch) || ch == '_') {
+          key += ch;
+          state = PARSE_KEY;
+        } else if (!std::isspace(ch)) {
+          state = IGNORE_REST;
+        }
+        break;
+
+      case PARSE_KEY:
+        if (ch == '=') {
+          state = FOUND_EQ;
+        } else if (std::isalnum(ch) || ch == '_') {
+          key += ch;
+        } else {
+          state = IGNORE_REST;
+        }
+        break;
+
+      case FOUND_EQ:
+        switch (ch) {
+          case '\'':
+            state = PARSE_SINGLE_QUOTE_VALUE;
+            break;
+          case '"':
+            state = PARSE_DBL_QUOTE_VALUE;
+            break;
+          case '#':
+          case '\\':
+            state = IGNORE_REST;
+            break;
+          default:
+            value += ch;
+            state = PARSE_VALUE;
+        }
+        break;
+
+      case PARSE_SINGLE_QUOTE_VALUE:
+        if (ch == '\'') {
+          if (prev != '\\') {
+            state = IGNORE_REST;
+          } else {
+            assert(!value.empty());
+            value[value.size() - 1] = ch;
+          }
+        } else {
+          value += ch;
+        }
+        break;
+
+      case PARSE_DBL_QUOTE_VALUE:
+        if (ch == '"') {
+          if (prev != '\\') {
+            state = IGNORE_REST;
+          } else {
+            assert(!value.empty());
+            value[value.size() - 1] = ch;
+          }
+        } else {
+          value += ch;
+        }
+        break;
+
+      case PARSE_VALUE:
+        if (ch == '#' || std::isspace(ch)) {
+          state = IGNORE_REST;
+        } else {
+          value += ch;
+        }
+        break;
+
+      default:
+        // Unexpected os-release parser state!
+        state = IGNORE_REST;
+        break;
+    }
+
+    if (state == IGNORE_REST) {
+      break;
+    }
+    prev = ch;
+  }
+  if (!(key.empty() || value.empty())) {
+    return std::make_pair(key, value);
+  }
+  return {};
+}
+
+std::map<std::string, std::string> GetOSReleaseVariables(
+  cmExecutionStatus& status)
+{
+  const auto& sysroot =
+    status.GetMakefile().GetSafeDefinition("CMAKE_SYSROOT");
+
+  std::map<std::string, std::string> data;
+  // Based on
+  // https://www.freedesktop.org/software/systemd/man/os-release.html
+  for (auto name : { "/etc/os-release"_s, "/usr/lib/os-release"_s }) {
+    const auto& filename = cmStrCat(sysroot, name);
+    if (cmSystemTools::FileExists(filename)) {
+      cmsys::ifstream fin(filename.c_str());
+      for (std::string line; !std::getline(fin, line).fail();) {
+        auto kv = ParseOSReleaseLine(line);
+        if (kv.has_value()) {
+          data.emplace(kv.value());
+        }
+      }
+      break;
+    }
+  }
+  return data;
+}
+
+cm::optional<std::string> GetValue(cmExecutionStatus& status,
+                                   std::string const& key,
+                                   std::string const& variable)
+{
+  const auto prefix = "DISTRIB_"_s;
+  if (!cmHasPrefix(key, prefix)) {
+    return {};
+  }
+
+  static const std::map<std::string, std::string> s_os_release =
+    GetOSReleaseVariables(status);
+
+  auto& makefile = status.GetMakefile();
+
+  const std::string subkey =
+    key.substr(prefix.size(), key.size() - prefix.size());
+  if (subkey == "INFO"_s) {
+    std::string vars;
+    for (const auto& kv : s_os_release) {
+      auto cmake_var_name = cmStrCat(variable, '_', kv.first);
+      vars += DELIM[!vars.empty()] + cmake_var_name;
+      makefile.AddDefinition(cmake_var_name, kv.second);
+    }
+    return cm::optional<std::string>(std::move(vars));
+  }
+
+  // Query individual variable
+  const auto it = s_os_release.find(subkey);
+  if (it != s_os_release.cend()) {
+    return it->second;
+  }
+
+  // NOTE Empty string means requested variable not set
+  return std::string{};
+}
+#endif
+
 #ifdef HAVE_VS_SETUP_HELPER
 cm::optional<std::string> GetValue(cmExecutionStatus& status,
                                    std::string const& key)
@@ -201,10 +381,9 @@ bool cmCMakeHostSystemInformationCommand(std::vector<std::string> const& args,
 
   std::string result_list;
   for (auto i = current_index + 1; i < args.size(); ++i) {
+    result_list += DELIM[!result_list.empty()];
+
     auto const& key = args[i];
-    if (i != current_index + 1) {
-      result_list += ";";
-    }
     auto value = GetValue(info, key);
     if (!value) {
 #ifdef HAVE_VS_SETUP_HELPER
@@ -213,6 +392,12 @@ bool cmCMakeHostSystemInformationCommand(std::vector<std::string> const& args,
         status.SetError("does not recognize <key> " + key);
         return false;
       }
+#elif defined(__linux__)
+      value = GetValue(status, key, variable);
+      if (!value) {
+        status.SetError("does not recognize <key> " + key);
+        return false;
+      }
 #else
       status.SetError("does not recognize <key> " + key);
       return false;

+ 1 - 1
Tests/RunCMake/CMakeLists.txt

@@ -349,7 +349,7 @@ if(NOT CMake_TEST_EXTERNAL_CMAKE)
 endif()
 add_RunCMake_test(execute_process)
 add_RunCMake_test(export)
-add_RunCMake_test(cmake_host_system_information)
+add_RunCMake_test(cmake_host_system_information -DCMAKE_SYSTEM_NAME=${CMAKE_SYSTEM_NAME})
 add_RunCMake_test(cmake_language)
 add_RunCMake_test(cmake_minimum_required)
 add_RunCMake_test(cmake_parse_arguments)

+ 9 - 0
Tests/RunCMake/cmake_host_system_information/Exherbo-stdout.txt

@@ -0,0 +1,9 @@
+-- TEST1_ANSI_COLOR=`0;32`
+-- TEST1_BUG_REPORT_URL=`https://bugs.exherbo.org/`
+-- TEST1_HOME_URL=`https://www.exherbo.org/`
+-- TEST1_ID=`exherbo`
+-- TEST1_NAME=`Exherbo`
+-- TEST1_PRETTY_NAME=`Exherbo Linux`
+-- TEST1_SUPPORT_URL=`irc://irc.freenode.net/#exherbo`
+-- TEST2_ID=`exherbo`
+-- TEST2_VERSION=``

+ 11 - 0
Tests/RunCMake/cmake_host_system_information/Exherbo.cmake

@@ -0,0 +1,11 @@
+cmake_host_system_information(RESULT TEST1 QUERY DISTRIB_INFO)
+
+foreach(VAR IN LISTS TEST1)
+  message(STATUS "${VAR}=`${${VAR}}`")
+endforeach()
+
+# Query individual variables
+cmake_host_system_information(RESULT TEST2 QUERY DISTRIB_ID DISTRIB_VERSION)
+list(POP_FRONT TEST2 TEST2_ID TEST2_VERSION)
+message(STATUS "TEST2_ID=`${TEST2_ID}`")
+message(STATUS "TEST2_VERSION=`${TEST2_VERSION}`")

+ 7 - 0
Tests/RunCMake/cmake_host_system_information/Exherbo/etc/os-release

@@ -0,0 +1,7 @@
+NAME="Exherbo"
+PRETTY_NAME="Exherbo Linux"
+ID="exherbo"
+ANSI_COLOR="0;32"
+HOME_URL="https://www.exherbo.org/"
+SUPPORT_URL="irc://irc.freenode.net/#exherbo"
+BUG_REPORT_URL="https://bugs.exherbo.org/"

+ 8 - 0
Tests/RunCMake/cmake_host_system_information/RunCMakeTest.cmake

@@ -6,3 +6,11 @@ run_cmake(BadArg3)
 
 run_cmake(QueryList)
 run_cmake(QueryKeys)
+
+if (CMAKE_SYSTEM_NAME MATCHES "Linux")
+
+  run_cmake_with_options(UnitTest)
+  run_cmake_with_options(Exherbo)
+  run_cmake_with_options(Ubuntu)
+
+endif()

+ 14 - 0
Tests/RunCMake/cmake_host_system_information/Ubuntu-stdout.txt

@@ -0,0 +1,14 @@
+-- TEST1_BUG_REPORT_URL=`https://bugs\.launchpad\.net/ubuntu/`
+-- TEST1_HOME_URL=`https://www\.ubuntu\.com/`
+-- TEST1_ID=`ubuntu`
+-- TEST1_ID_LIKE=`debian`
+-- TEST1_NAME=`Ubuntu`
+-- TEST1_PRETTY_NAME=`Ubuntu 20\.04\.2 LTS`
+-- TEST1_PRIVACY_POLICY_URL=`https://www\.ubuntu\.com/legal/terms-and-policies/privacy-policy`
+-- TEST1_SUPPORT_URL=`https://help\.ubuntu\.com/`
+-- TEST1_UBUNTU_CODENAME=`focal`
+-- TEST1_VERSION=`20\.04\.2 LTS \(Focal Fossa\)`
+-- TEST1_VERSION_CODENAME=`focal`
+-- TEST1_VERSION_ID=`20\.04`
+-- TEST2_ID=`ubuntu`
+-- TEST2_VERSION=`20\.04\.2 LTS \(Focal Fossa\)`

+ 11 - 0
Tests/RunCMake/cmake_host_system_information/Ubuntu.cmake

@@ -0,0 +1,11 @@
+cmake_host_system_information(RESULT TEST1 QUERY DISTRIB_INFO)
+
+foreach(VAR IN LISTS TEST1)
+  message(STATUS "${VAR}=`${${VAR}}`")
+endforeach()
+
+# Query individual variables
+cmake_host_system_information(RESULT TEST2 QUERY DISTRIB_ID DISTRIB_VERSION)
+list(POP_FRONT TEST2 TEST2_ID TEST2_VERSION)
+message(STATUS "TEST2_ID=`${TEST2_ID}`")
+message(STATUS "TEST2_VERSION=`${TEST2_VERSION}`")

+ 12 - 0
Tests/RunCMake/cmake_host_system_information/Ubuntu/etc/os-release

@@ -0,0 +1,12 @@
+NAME="Ubuntu"
+VERSION="20.04.2 LTS (Focal Fossa)"
+ID=ubuntu
+ID_LIKE=debian
+PRETTY_NAME="Ubuntu 20.04.2 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

+ 7 - 0
Tests/RunCMake/cmake_host_system_information/UnitTest-stdout.txt

@@ -0,0 +1,7 @@
+-- UNIT_TEST_A_LIST_LIKE_VARIABLE=`satu;dua;tiga`
+-- UNIT_TEST_DBL_QUOTED_VALUE=`"The" value in double "quotes"`
+-- UNIT_TEST_DBL_QUOTED_VALUE_STIPPED_COMMENT=`Blah blah blah`
+-- UNIT_TEST_NON_SPACE_VALUE=`Blah-blah-blah`
+-- UNIT_TEST_QUOTED_VALUE=`'The' value in single 'quotes'`
+-- UNIT_TEST_QUOTED_VALUE_STIPPED_COMMENT=`The value in single quotes`
+-- UNIT_TEST_THE_URL_WITH_ANCHOR_TEST=`https://blah.blah/resource#anchor`

+ 5 - 0
Tests/RunCMake/cmake_host_system_information/UnitTest.cmake

@@ -0,0 +1,5 @@
+cmake_host_system_information(RESULT UNIT_TEST QUERY DISTRIB_INFO)
+
+foreach(VAR IN LISTS UNIT_TEST)
+  message(STATUS "${VAR}=`${${VAR}}`")
+endforeach()

+ 9 - 0
Tests/RunCMake/cmake_host_system_information/UnitTest/etc/os-release

@@ -0,0 +1,9 @@
+# Comment string gonna be ignored
+NON_SPACE_VALUE=Blah-blah-blah
+QUOTED_VALUE='\'The\' value in single \'quotes\''
+QUOTED_VALUE_STIPPED_COMMENT='The value in single quotes'# The comment right after `'`
+DBL_QUOTED_VALUE="\"The\" value in double \"quotes\""
+DBL_QUOTED_VALUE_STIPPED_COMMENT="Blah blah blah"# The comment right after `'`
+THE_URL_WITH_ANCHOR_TEST="https://blah.blah/resource#anchor" # And a comment after
+A_LIST_LIKE_VARIABLE='satu;dua;tiga'
+INCORRECT_ESCAPE_IGNORED=\'This line gonna be ignored'