From ee0062a436a7955dd883abb3a09e16f038dc616c Mon Sep 17 00:00:00 2001
From: Sam Yates <halfflat@gmail.com>
Date: Thu, 11 Mar 2021 14:20:39 +0100
Subject: [PATCH] Extend available version information. (#1411)

* Provide defines and variables for version string components: major
version; minor version; patch level version; development suffix.
* Change commit date format to strict ISO.
* Add define/variable for Arbor build configuration.
* Add usage/help info to `git-source-id` script.
* Add define/variable for a full build identification string,
incorporating source info, version, build configuration, feature
options.
* Add documentation for the version and build defines and variables.
* Extend version unit tests to suit.
---
 arbor/include/CMakeLists.txt |  4 +-
 arbor/include/git-source-id  | 62 ++++++++++++++++++++++++-----
 arbor/version.cpp            | 13 ++++--
 doc/cpp/index.rst            |  1 +
 doc/cpp/version.rst          | 77 ++++++++++++++++++++++++++++++++++++
 test/unit/test_version.cpp   | 64 +++++++++++++++++++++++++++++-
 6 files changed, 205 insertions(+), 16 deletions(-)
 create mode 100644 doc/cpp/version.rst

diff --git a/arbor/include/CMakeLists.txt b/arbor/include/CMakeLists.txt
index b61d4a6d..fde67911 100644
--- a/arbor/include/CMakeLists.txt
+++ b/arbor/include/CMakeLists.txt
@@ -50,10 +50,12 @@ if(ARB_VECTORIZE)
     list(APPEND arb_features VECTORIZE)
 endif()
 
+string(TOUPPER "${CMAKE_BUILD_TYPE}" arb_config_str)
+
 add_custom_command(
     OUTPUT version.hpp-test
     DEPENDS _always_rebuild
-    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/git-source-id ${FULL_VERSION_STRING} ${ARB_ARCH} ${arb_features} > version.hpp-test
+    COMMAND ${CMAKE_CURRENT_SOURCE_DIR}/git-source-id ${FULL_VERSION_STRING} ${ARB_ARCH} ${arb_config_str} ${arb_features} > version.hpp-test
 )
 
 set(version_hpp_path arbor/version.hpp)
diff --git a/arbor/include/git-source-id b/arbor/include/git-source-id
index 7946b823..c3fc3ec9 100755
--- a/arbor/include/git-source-id
+++ b/arbor/include/git-source-id
@@ -1,31 +1,73 @@
 #!/usr/bin/env bash
 
-# arguments: version-string [feature...]
+function usage () {
+    helpstr="Usage: ${0##*/} version arch config [feature ...]"
+    if [ -n "$1" ]; then
+        echo "${0##*/}: $1" >&2
+        echo "$helpstr" >&2
+        exit 1
+    else
+        echo "$helpstr"
+        exit 0
+    fi
+}
+
+if [ -z "$1" -o "$1" = "--help" -o "$1" = "-h" ]; then usage; fi
+if [ -z "$2" ]; then usage "missing arch"; fi
+if [ -z "$3" ]; then usage "missing config"; fi
 
 version="$1"
 arch="$2"
-shift 2
+config="$3"
+shift 3
+
+if [[ "$version" =~ ^([0-9]+)\.([0-9]+)(\.([0-9]+))?(-(.*))?$ ]]; then
+    version_major="${BASH_REMATCH[1]}"
+    version_minor="${BASH_REMATCH[2]}"
+    version_patch="${BASH_REMATCH[4]:-0}"
+    version_dev="${BASH_REMATCH[6]}"
+else
+    usage "bad version format"
+fi
 
-if gitlog=$(git log -1 --pretty=format:'%ci %H' 2>/dev/null); then
+if gitlog=$(git log -1 --pretty=format:'%cI %H' 2>/dev/null); then
     git diff --quiet HEAD 2>/dev/null || gitlog="${gitlog} modified"
 else
     gitlog='unknown commit'
 fi
 
+full_build_id="source_id=${gitlog};version=${version};arch=${arch};config=${config};"
+for feature in "$@"; do
+    full_build_id="${full_build_id}${feature}_ENABLED;"
+done
+
 cat << __end__
 #pragma once
 
-#define ARB_VERSION "${version}"
-#define ARB_SOURCE_ID "${gitlog}"
-#define ARB_ARCH "${arch}"
-
 namespace arb {
-extern const char version[];
-extern const char source_id[];
-extern const char arch[];
+extern const char* source_id;
+extern const char* arch;
+extern const char* build_config;
+extern const char* version;
+extern const char* full_build_id;
+constexpr int version_major = ${version_major};
+constexpr int version_minor = ${version_minor};
+constexpr int version_patch = ${version_patch};
+extern const char* version_dev;
 }
+
+#define ARB_SOURCE_ID "${gitlog}"
+#define ARB_ARCH "${arch}"
+#define ARB_BUILD_CONFIG "${config}"
+#define ARB_FULL_BUILD_ID "${full_build_id}"
+#define ARB_VERSION "${version}"
+#define ARB_VERSION_MAJOR ${version_major}
+#define ARB_VERSION_MINOR ${version_minor}
+#define ARB_VERSION_PATCH ${version_patch}
 __end__
 
+if [ -n "$version_dev" ]; then echo "#define ARB_VERSION_DEV \"${version_dev}\""; fi
+
 for feature in "$@"; do
     echo "#define ARB_${feature}_ENABLED"
 done
diff --git a/arbor/version.cpp b/arbor/version.cpp
index e3b689cf..0b2233a2 100644
--- a/arbor/version.cpp
+++ b/arbor/version.cpp
@@ -1,7 +1,14 @@
 #include <arbor/version.hpp>
 
 namespace arb {
-const char version[] = ARB_VERSION;
-const char source_id[] = ARB_SOURCE_ID;
-const char arch[] = ARB_ARCH;
+const char* source_id = ARB_SOURCE_ID;
+const char* arch = ARB_ARCH;
+const char* build_config = ARB_BUILD_CONFIG;
+const char* version = ARB_VERSION;
+#ifdef ARB_VERSION_DEV
+const char* version_dev = ARB_VERSION_DEV;
+#else
+const char* version_dev = "";
+#endif
+const char* full_build_id = ARB_FULL_BUILD_ID;
 }
diff --git a/doc/cpp/index.rst b/doc/cpp/index.rst
index bd97b673..f4f437ec 100644
--- a/doc/cpp/index.rst
+++ b/doc/cpp/index.rst
@@ -23,3 +23,4 @@ A :cpp:type:`arb::recipe` describes a model, and a :cpp:type:`arb::simulation` i
    simulation
    profiler
    cable_cell
+   version
diff --git a/doc/cpp/version.rst b/doc/cpp/version.rst
new file mode 100644
index 00000000..05d56e09
--- /dev/null
+++ b/doc/cpp/version.rst
@@ -0,0 +1,77 @@
+.. _cppversion:
+
+Version and build information
+=============================
+
+The Arbor library records version and configuration information in
+two ways:
+
+* The ``version.hpp`` header has preprocessor defines with the prefix ``ARB_``.
+
+* The library presents this information in variables within the ``arb::`` namespace.
+
+Version information
+-------------------
+
+The Arbor version string is in the format MAJOR.MINOR.PATCH,
+or for development versions MAJOR.MINOR.PATCH-DEV, where DEV
+is a string, usually literally "dev".
+
+:c:macro:`ARB_VERSION`
+    Full Arbor version string. Available as :cpp:var:`arb::version`.
+
+:c:macro:`ARB_VERSION_MAJOR`
+    Major version number. Available as :cpp:var:`arb::version_major`.
+
+:c:macro:`ARB_VERSION_MINOR`
+    Major version number. Available as :cpp:var:`arb::version_minor`.
+
+:c:macro:`ARB_VERSION_PATCH`
+    Major version number. Available as :cpp:var:`arb::version_patch`.
+
+:c:macro:`ARB_VERSION_DEV`
+    Development version suffix string. Only defined if Arbor is a development version.
+    Available as :cpp:var:`arb::version_dev`, which will be an empty string
+    if :c:macro:`ARB_VERSION_DEV` is not defined.
+
+Source information
+------------------
+
+:c:macro:`ARB_SOURCE_ID`
+   The source id contains the git commit time stamp, the commit hash,
+   and if there are uncommitted changes in the source tree, a suffix "modified",
+   e.g. ``"2020-01-02T03:04:05+06:00 b1946ac92492d2347c6235b4d2611184 modified"``.
+   Available as :cpp:var:`arb::source_id`.
+
+Build information
+-----------------
+
+Arbor can be built in the default 'Release' configuration, or in an unoptimized
+'Debug' configuration that is useful for development. Additionally, it can be
+built for a particular CPU architecture given by the ``ARB_ARCH`` CMake configuration
+variable.
+
+:c:macro:`ARB_BUILD_CONFIG`
+    Configuration string, all uppercase. Will be ``"DEBUG"`` or ``"RELEASE"``.
+    Available as :cpp:var:`arb::build_config`.
+
+:c:macro:`ARB_ARCH`
+    Value of the ``ARB_ARCH`` configuration variable, e.g. ``"native"``.
+    Available as :cpp:var:`arb::arch`.
+
+Features
+--------
+
+Configuration-time features are enabled in Arbor via CMake configuration variables
+such as ``ARB_WITH_MPI`` and ``ARB_WITH_PYTHON``. Each enabled feature has
+a corresponding preprocessor symbol in ``version.hpp`` of the form ``ARB_FEATURENAME_ENABLED``.
+Examples include :c:macro:`ARB_MPI_ENABLED`, :c:macro:`ARB_ASSERT_ENABLED`.
+
+Full build information
+----------------------
+
+A single string containing all the identification information for an Arbor build
+is available in the macro :c:macro:`ARB_FULL_BUILD_ID` and in the variable
+:cpp:var:`arb::full_build_id`. This string contains the source id, the full version,
+the build configuration, the target architecture, and a list of enabled features.
+
diff --git a/test/unit/test_version.cpp b/test/unit/test_version.cpp
index cdf42304..05f5ebe0 100644
--- a/test/unit/test_version.cpp
+++ b/test/unit/test_version.cpp
@@ -1,18 +1,78 @@
+#include <regex>
 #include <string>
 
 #include "../gtest.h"
 
 #include <arbor/version.hpp>
 
-TEST(version, libmatch) {
-    using std::string;
+using namespace std::string_literals;
+using std::regex;
+using std::regex_search;
+using std::string;
+using std::to_string;
 
+TEST(version, libmatch) {
     string header_version = ARB_VERSION;
     string header_source_id = ARB_SOURCE_ID;
+    string header_arch = ARB_ARCH;
+    string header_build_config = ARB_BUILD_CONFIG;
+    string header_full_build_id = ARB_FULL_BUILD_ID;
+#ifdef ARB_VERSION_DEV
+    string header_version_dev = ARB_VERSION_DEV;
+    EXPECT_FALSE(header_version_dev.empty());
+#else
+    string header_version_dev;
+#endif
+    int header_version_major = ARB_VERSION_MAJOR;
+    int header_version_minor = ARB_VERSION_MINOR;
+    int header_version_patch = ARB_VERSION_PATCH;
 
     string lib_version = arb::version;
     string lib_source_id = arb::source_id;
+    string lib_arch = arb::arch;
+    string lib_build_config = arb::build_config;
+    string lib_full_build_id = arb::full_build_id;
+    constexpr int lib_version_major = arb::version_major;
+    constexpr int lib_version_minor = arb::version_minor;
+    constexpr int lib_version_patch = arb::version_patch;
+    string lib_version_dev = arb::version_dev;
 
     EXPECT_EQ(header_version, lib_version);
     EXPECT_EQ(header_source_id, lib_source_id);
+    EXPECT_EQ(header_arch, lib_arch);
+    EXPECT_EQ(header_build_config, lib_build_config);
+    EXPECT_EQ(header_full_build_id, lib_full_build_id);
+    EXPECT_EQ(header_version_major, lib_version_major);
+    EXPECT_EQ(header_version_minor, lib_version_minor);
+    EXPECT_EQ(header_version_patch, lib_version_patch);
+    EXPECT_EQ(header_version_dev, lib_version_dev);
+}
+
+TEST(version, sane_config) {
+    EXPECT_TRUE(arb::build_config=="DEBUG"s || arb::build_config=="RELEASE"s);
+}
+
+TEST(version, version_components) {
+    string dev = arb::version_dev;
+
+    if (arb::version_patch>0) {
+        auto expected = to_string(arb::version_major)+"."+to_string(arb::version_minor)+"."+to_string(arb::version_patch);
+        expected += dev.empty()? "": "-"+dev;
+
+        EXPECT_EQ(expected, ARB_VERSION);
+    }
+    else {
+        auto expected_majmin = to_string(arb::version_major)+"."+to_string(arb::version_minor);
+        auto expected_suffix = dev.empty()? "": "-"+dev;
+
+        EXPECT_TRUE(expected_majmin+expected_suffix==ARB_VERSION || expected_majmin+".0"+expected_suffix==ARB_VERSION);
+    }
 }
+
+TEST(version, full_build_id) {
+    EXPECT_TRUE(regex_search(arb::full_build_id, regex("(;|^)config=.")));
+    EXPECT_TRUE(regex_search(arb::full_build_id, regex("(;|^)version=.")));
+    EXPECT_TRUE(regex_search(arb::full_build_id, regex("(;|^)source_id=.")));
+    EXPECT_TRUE(regex_search(arb::full_build_id, regex("(;|^)arch=.")));
+}
+
-- 
GitLab