build FEATURE ABI check (#1143)

* build FEATURE dump and check ABI information to have a proper version

* travis CHANGE integrate ABI check into Travis CI

* travis CHANGE remove unnecessary libpcre2-dev install

Xenial includes old version in packages, so we have to install pcre2 from sources

* travis CHANGE install cmocka in OSX from package
diff --git a/.travis.yml b/.travis.yml
index 3b6b121..1447a3a 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,6 +14,12 @@
   - name: Coverity
     if: type = cron
 
+addons:
+  homebrew:
+    packages:
+    - cmocka
+    update: true
+
 jobs:
   include:
     - stage: Coverity
@@ -33,7 +39,6 @@
           build_command: "make"
           branch_pattern: libyang2
       before_install:
-        - sudo apt-get install libpcre2-dev
         # check if something changed from the last coverity build
         - echo "Last coverity build on revision" `cat $HOME/cache/coveritybuild 2>/dev/null`
         - echo "Current revision" `git rev-parse HEAD`
@@ -84,16 +89,23 @@
       after_success:
         - bash <(curl -s https://codecov.io/bash)
     - stage: Test
+      name: ABI check
+      os: linux
+      compiled: gcc
+      before_install:
+        - sudo apt-get update -qq && sudo apt-get install -y abi-dumper abi-compliance-checker
+        - sudo snap install core universal-ctags
+        - wget https://ftp.pcre.org/pub/pcre/pcre2-10.30.tar.gz
+        - tar -xzf pcre2-10.30.tar.gz
+        - cd pcre2-10.30 && ./configure && make -j2 && sudo make install && cd ..
+      script:
+        - mkdir build && cd build && cmake -DCMAKE_BUILD_TYPE=ABICheck .. && LC_ALL=C.UTF-8 PATH=/snap/bin:$PATH make abi-check && cd -
+    - stage: Test
       name: OS X with GCC
       os: osx
       compiler: gcc
       allow_failures:
         - os: osx
-      before_install:
-        - wget https://cmocka.org/files/1.1/cmocka-1.1.2.tar.xz
-        - tar -xf cmocka-1.1.2.tar.xz
-        - cd cmocka-1.1.2 && mkdir build && cd build && cmake .. && make -j2 && sudo make install && cd ../..
-        - brew update
       script:
         - mkdir build && cd build && cmake -DENABLE_VALGRIND_TESTS=OFF .. && make -j2 && ctest --output-on-failure && cd -
-
+ 
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 85117c3..5bd67a4 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -13,6 +13,7 @@
 include(GNUInstallDirs)
 include(CheckSymbolExists)
 include(UseCompat)
+include(ABICheck)
 
 set(LIBYANG_DESCRIPTION "libyang is YANG data modelling language parser and toolkit written (and providing API) in C.")
 
@@ -33,11 +34,26 @@
 
 # set default build type if not specified by user
 if(NOT CMAKE_BUILD_TYPE)
-    set(CMAKE_BUILD_TYPE debug)
+    set(CMAKE_BUILD_TYPE Debug)
 endif()
+# normalize build type string
+string(TOUPPER "${CMAKE_BUILD_TYPE}" BUILD_TYPE_UPPER)
+if ("${BUILD_TYPE_UPPER}" STREQUAL "RELEASE")
+  set(CMAKE_BUILD_TYPE "Release" CACHE STRING "Build Type" FORCE)
+elseif ("${BUILD_TYPE_UPPER}" STREQUAL "DEBUG")
+  set(CMAKE_BUILD_TYPE "Debug" CACHE STRING "Build Type" FORCE)
+elseif ("${BUILD_TYPE_UPPER}" STREQUAL "RELWITHDEBINFO")
+  set(CMAKE_BUILD_TYPE "RelWithDebInfo" CACHE STRING "Build Type" FORCE)
+elseif ("${BUILD_TYPE_UPPER}" STREQUAL "RELWITHDEBUG")
+  set(CMAKE_BUILD_TYPE "RelWithDebug" CACHE STRING "Build Type" FORCE)
+elseif ("${BUILD_TYPE_UPPER}" STREQUAL "ABICHECK")
+  set(CMAKE_BUILD_TYPE "ABICheck" CACHE STRING "Build Type" FORCE)
+else ()
+  message(FATAL_ERROR "Unknown CMAKE_BUILD_TYPE \"${CMAKE_BUILD_TYPE}\".")
+endif ()
 
 # options
-if((CMAKE_BUILD_TYPE STREQUAL debug) OR (CMAKE_BUILD_TYPE STREQUAL Package))
+if(("${BUILD_TYPE_UPPER}" STREQUAL "DEBUG") OR ("${BUILD_TYPE_UPPER}" STREQUAL "RELWITHDEBINFO"))
     option(ENABLE_BUILD_TESTS "Build tests" ON)
     option(ENABLE_VALGRIND_TESTS "Build tests with valgrind" ON)
     set(INTERNAL_DOCS YES)
@@ -47,7 +63,6 @@
     set(INTERNAL_DOCS NO)
 endif()
 option(ENABLE_COVERAGE "Build code coverage report from tests" OFF)
-
 option(ENABLE_FUZZ_TARGETS "Build target programs suitable for fuzzing with AFL" OFF)
 #option(ENABLE_CALLGRIND_TESTS "Build performance tests to be run with callgrind" OFF)
 
@@ -105,10 +120,9 @@
     endif()
 endif()
 
-set(CMAKE_C_FLAGS         "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_COVERAGE} -Wall -Wextra -Wno-missing-field-initializers -std=c99")
-set(CMAKE_C_FLAGS_RELEASE "-O2 -DNDEBUG")
-set(CMAKE_C_FLAGS_PACKAGE "-g -O2 -DNDEBUG")
-set(CMAKE_C_FLAGS_DEBUG   "-g -Og")
+set(CMAKE_C_FLAGS                "${CMAKE_C_FLAGS} ${CMAKE_C_FLAGS_COVERAGE} -Wall -Wextra -Wno-missing-field-initializers -std=c99")
+set(CMAKE_C_FLAGS_DEBUG          "-g3 -O0")
+set(CMAKE_C_FLAGS_ABICHECK       "-g -Og")
 
 include_directories(${PROJECT_BINARY_DIR}/src ${PROJECT_SOURCE_DIR}/src)
 configure_file(${PROJECT_SOURCE_DIR}/src/config.h.in ${PROJECT_BINARY_DIR}/src/config.h @ONLY)
@@ -279,6 +293,11 @@
     configure_file(Doxyfile.in Doxyfile)
 endif()
 
+# generate API/ABI report
+if ("${BUILD_TYPE_UPPER}" STREQUAL "ABICHECK")
+	libyang_abicheck()
+endif()
+
 # clean cmake cache
 add_custom_target(cclean
         COMMAND make clean
@@ -291,9 +310,9 @@
 # if the tests are enabled, build libyang_ext_test
 if(ENABLE_BUILD_TESTS)
     find_package(CMocka 1.0.0)
-#    if(CMOCKA_FOUND AND CMAKE_BUILD_TYPE MATCHES debug)
+#    if(CMOCKA_FOUND AND CMAKE_BUILD_TYPE MATCHES Debug)
 #        list(APPEND EXTENSIONS_LIST "libyang_ext_test")
-#    endif(CMOCKA_FOUND AND CMAKE_BUILD_TYPE MATCHES debug)
+#    endif(CMOCKA_FOUND AND CMAKE_BUILD_TYPE MATCHES Debug)
 endif(ENABLE_BUILD_TESTS)
 
 #if(ENABLE_STATIC)
diff --git a/CMakeModules/ABICheck.cmake b/CMakeModules/ABICheck.cmake
new file mode 100644
index 0000000..bd46415
--- /dev/null
+++ b/CMakeModules/ABICheck.cmake
@@ -0,0 +1,69 @@
+cmake_minimum_required(VERSION 2.8.12)
+
+# generate API/ABI report
+macro(LIBYANG_ABICHECK)
+	find_program(ABI_DUMPER abi-dumper)
+	find_package_handle_standard_args(abi-dumper DEFAULT_MSG ABI_DUMPER)
+	if(ABI_DUMPER)
+	    set(PUBLIC_HEADERS ${headers})
+	    string(PREPEND PUBLIC_HEADERS "${CMAKE_SOURCE_DIR}/")
+	    string(REPLACE ";" "\n${CMAKE_SOURCE_DIR}/" PUBLIC_HEADERS "${PUBLIC_HEADERS}")
+	    file(GENERATE OUTPUT ${CMAKE_BINARY_DIR}/public_headers CONTENT "${PUBLIC_HEADERS}")
+	    add_custom_target(abi-dump
+	            COMMAND ${ABI_DUMPER} ./libyang${CMAKE_SHARED_LIBRARY_SUFFIX} -o libyang.${LIBYANG_SOVERSION_FULL}.dump
+	            -lver ${LIBYANG_SOVERSION_FULL} -public-headers ${CMAKE_BINARY_DIR}/public_headers
+	            DEPENDS yang
+	            BYPRODUCTS ${CMAKE_BINARY_DIR}/libyang.${LIBYANG_SOVERSION_FULL}.dump
+	            WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+	            COMMENT Dumping ABI information for abi-check.)
+	endif()
+	
+	# check correctness of the SO version according to the API/ABI changes
+	find_program(ABI_CHECKER abi-compliance-checker)
+	find_package_handle_standard_args(abi-compliance-checker DEFAULT_MSG ABI_CHECKER)
+	if(ABI_DUMPER AND ABI_CHECKER)
+		set(ABIBASE_HASH "" CACHE STRING "GIT hash for the commit to compare current ABI to. If not set, base SO version commit is found.")
+		if (NOT ABIBASE_HASH)
+		    # check that we have some ABI base version already and get its hash in GIT
+		    set(ABIBASE_VERSION "${LIBYANG_MAJOR_VERSION}.0.0")
+		    execute_process(COMMAND bash "-c" "git log --pretty=oneline --grep=\"SOVERSION .* ${ABIBASE_VERSION}\$\" | cut -d' ' -f1"
+		            OUTPUT_VARIABLE ABIBASE_HASH)
+		else()
+			string(SUBSTRING ${ABIBASE_HASH} 0 8 ABIBASE_HASH_SHORT)
+			set(ABIBASE_VERSION "git-${ABIBASE_HASH_SHORT}")
+		endif()
+	    if(ABIBASE_HASH)
+	        # we have the ABI base version, so generate script for abi-check target
+	        string(REPLACE "\n" "" ABIBASE_HASH ${ABIBASE_HASH})
+	        file(GENERATE OUTPUT ${CMAKE_BINARY_DIR}/abibase.sh CONTENT "#!/bin/sh
+if [ ! -d abibase ]; then mkdir abibase; fi
+cd abibase
+if [ ! -f build/libyang.*.dump ]; then
+    if [ -d .git ] && [ \"${ABIBASE_HASH}\" != \"`git log --pretty=oneline | cut -d' ' -f1`\" ]; then rm -rf .* 2> /dev/null; fi
+    if [ ! -d .git ]; then
+        git init
+        git remote add origin https://github.com/CESNET/libyang
+        git fetch origin --depth 1 ${ABIBASE_HASH}
+        git reset --hard FETCH_HEAD
+    fi
+    if [ ! -d build ]; then mkdir build; fi
+    cd build
+    cmake -DCMAKE_BUILD_TYPE=ABICheck ..
+    make abi-dump
+fi
+")
+	        # abi-check target itself using abi-compliance-checker
+	        add_custom_target(abi-check
+	                COMMAND bash ./abibase.sh
+	                COMMAND ${ABI_CHECKER} -l libyang${CMAKE_SHARED_LIBRARY_SUFFIX} -old abibase/build/libyang.*.dump
+	                -new ./libyang.${LIBYANG_SOVERSION_FULL}.dump -s
+	                DEPENDS yang abi-dump
+	                BYPRODUCTS ${CMAKE_BINARY_DIR}/compat_reports/libyang${CMAKE_SHARED_LIBRARY_SUFFIX}/*_to_${LIBYANG_SOVERSION_FULL}/compat_report.html
+	                WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
+	                COMMENT Checking ABI compatibility with the ${ABIBASE_VERSION} version.)
+	    else()
+	        add_custom_target(abi-check
+	                COMMENT Nothing to check - missing base SOVERSION commit for ${ABIBASE_VERSION} version.)
+	    endif()
+	endif()
+endmacro()