From f5aa430e3a31bea5c3b864839ce477fe1fe6c082 Mon Sep 17 00:00:00 2001 From: Artem Dinaburg Date: Tue, 11 Aug 2020 02:03:37 -0400 Subject: [PATCH] Detailed Version Information (#684) Emit version information with git commit hash, commit date, message, and author. --- .dockerignore | 2 +- .gitignore | 6 + CMakeLists.txt | 5 +- cmake/git_watcher.cmake | 310 ++++++++++++++++++++++++++++++++++ mcsema/Version.h.in | 19 --- mcsema/Version/CMakeLists.txt | 16 ++ mcsema/Version/Version.cpp.in | 61 +++++++ mcsema/Version/Version.h | 44 +++++ tools/mcsema_lift/Lift.cpp | 44 +++-- 9 files changed, 468 insertions(+), 39 deletions(-) create mode 100644 cmake/git_watcher.cmake delete mode 100644 mcsema/Version.h.in create mode 100644 mcsema/Version/CMakeLists.txt create mode 100644 mcsema/Version/Version.cpp.in create mode 100644 mcsema/Version/Version.h diff --git a/.dockerignore b/.dockerignore index 9560e2e41..1617fbf89 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,6 +1,6 @@ build Dockerfile* .travis.yml -.git* +.github* LICENSE README.md diff --git a/.gitignore b/.gitignore index 4abb0448e..a4faf9b0f 100644 --- a/.gitignore +++ b/.gitignore @@ -69,3 +69,9 @@ package # Ignore local binary ninja installations binaryninja + +#ignore vscode dir +.vscode + +#ignore generated version files +VERSION diff --git a/CMakeLists.txt b/CMakeLists.txt index 20ec7940f..74b5556f0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -96,6 +96,9 @@ set(REMILL_LLVM_VERSION "${LLVM_MAJOR_VERSION}.${LLVM_MINOR_VERSION}") set(MCSEMA_LIFT mcsema-lift-${REMILL_LLVM_VERSION}) +# for version information +add_subdirectory(mcsema/Version) + add_executable(${MCSEMA_LIFT} ${PROJECT_PROTOBUFSOURCEFILES} @@ -200,7 +203,7 @@ list(APPEND PROJECT_LIBRARIES gflags) # target settings # -target_link_libraries(${MCSEMA_LIFT} PRIVATE ${PROJECT_LIBRARIES}) +target_link_libraries(${MCSEMA_LIFT} PRIVATE ${PROJECT_LIBRARIES} McSemaVersion) target_include_directories(${MCSEMA_LIFT} SYSTEM PUBLIC ${PROJECT_INCLUDEDIRECTORIES}) target_compile_definitions(${MCSEMA_LIFT} PUBLIC ${PROJECT_DEFINITIONS}) target_compile_options(${MCSEMA_LIFT} PRIVATE ${PROJECT_CXXFLAGS}) diff --git a/cmake/git_watcher.cmake b/cmake/git_watcher.cmake new file mode 100644 index 000000000..ab4b23a3b --- /dev/null +++ b/cmake/git_watcher.cmake @@ -0,0 +1,310 @@ +# git_watcher.cmake +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/git_watcher.cmake +# +# Released under the MIT License. +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE + + +# This file defines a target that monitors the state of a git repo. +# If the state changes (e.g. a commit is made), then a file gets reconfigured. +# Here are the primary variables that control script behavior: +# +# PRE_CONFIGURE_FILE (REQUIRED) +# -- The path to the file that'll be configured. +# +# POST_CONFIGURE_FILE (REQUIRED) +# -- The path to the configured PRE_CONFIGURE_FILE. +# +# GIT_STATE_FILE (OPTIONAL) +# -- The path to the file used to store the previous build's git state. +# Defaults to the current binary directory. +# +# GIT_WORKING_DIR (OPTIONAL) +# -- The directory from which git commands will be run. +# Defaults to the directory with the top level CMakeLists.txt. +# +# GIT_EXECUTABLE (OPTIONAL) +# -- The path to the git executable. It'll automatically be set if the +# user doesn't supply a path. +# +# DESIGN +# - This script was designed similar to a Python application +# with a Main() function. I wanted to keep it compact to +# simplify "copy + paste" usage. +# +# - This script is invoked under two CMake contexts: +# 1. Configure time (when build files are created). +# 2. Build time (called via CMake -P). +# The first invocation is what registers the script to +# be executed at build time. +# +# MODIFICATIONS +# You may wish to track other git properties like when the last +# commit was made. There are two sections you need to modify, +# and they're tagged with a ">>>" header. + +# Short hand for converting paths to absolute. +macro(PATH_TO_ABSOLUTE var_name) + get_filename_component(${var_name} "${${var_name}}" ABSOLUTE) +endmacro() + +# Check that a required variable is set. +macro(CHECK_REQUIRED_VARIABLE var_name) + if(NOT DEFINED ${var_name}) + message(FATAL_ERROR "The \"${var_name}\" variable must be defined.") + endif() + PATH_TO_ABSOLUTE(${var_name}) +endmacro() + +# Check that an optional variable is set, or, set it to a default value. +macro(CHECK_OPTIONAL_VARIABLE var_name default_value) + if(NOT DEFINED ${var_name}) + set(${var_name} ${default_value}) + endif() + PATH_TO_ABSOLUTE(${var_name}) +endmacro() + +CHECK_REQUIRED_VARIABLE(PRE_CONFIGURE_FILE) +CHECK_REQUIRED_VARIABLE(POST_CONFIGURE_FILE) +CHECK_OPTIONAL_VARIABLE(GIT_STATE_FILE "${CMAKE_BINARY_DIR}/git-state-hash") +CHECK_OPTIONAL_VARIABLE(GIT_WORKING_DIR "${CMAKE_SOURCE_DIR}") + +# Check the optional git variable. +# If it's not set, we'll try to find it using the CMake packaging system. +if(NOT DEFINED GIT_EXECUTABLE) + find_package(Git QUIET REQUIRED) +endif() +CHECK_REQUIRED_VARIABLE(GIT_EXECUTABLE) + + +set(_state_variable_names + GIT_RETRIEVED_STATE + GIT_HEAD_SHA1 + GIT_IS_DIRTY + GIT_AUTHOR_NAME + GIT_AUTHOR_EMAIL + GIT_COMMIT_DATE_ISO8601 + GIT_COMMIT_SUBJECT + GIT_COMMIT_BODY + VERSION_STRING + # >>> + # 1. Add the name of the additional git variable you're interested in monitoring + # to this list. +) + + + +# Macro: RunGitCommand +# Description: short-hand macro for calling a git function. Outputs are the +# "exit_code" and "output" variables. +macro(RunGitCommand) + execute_process(COMMAND + "${GIT_EXECUTABLE}" ${ARGV} + WORKING_DIRECTORY "${_working_dir}" + RESULT_VARIABLE exit_code + OUTPUT_VARIABLE output + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_RETRIEVED_STATE} "false") + endif() +endmacro() + + + +# Function: GetGitState +# Description: gets the current state of the git repo. +# Args: +# _working_dir (in) string; the directory from which git commands will be executed. +function(GetGitState _working_dir) + + # This is an error code that'll be set to FALSE if the + # RunGitCommand ever returns a non-zero exit code. + set(ENV{GIT_RETRIEVED_STATE} "true") + + # Get whether or not the working tree is dirty. + RunGitCommand(status --porcelain) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_IS_DIRTY} "false") + else() + if(NOT "${output}" STREQUAL "") + set(ENV{GIT_IS_DIRTY} "true") + else() + set(ENV{GIT_IS_DIRTY} "false") + endif() + endif() + + # There's a long list of attributes grabbed from git show. + set(object HEAD) + RunGitCommand(show -s "--format=%H" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_HEAD_SHA1} ${output}) + endif() + + RunGitCommand(show -s "--format=%an" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_AUTHOR_NAME} "${output}") + endif() + + RunGitCommand(show -s "--format=%ae" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_AUTHOR_EMAIL} "${output}") + endif() + + RunGitCommand(show -s "--format=%ci" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_COMMIT_DATE_ISO8601} "${output}") + endif() + + RunGitCommand(show -s "--format=%s" ${object}) + if(exit_code EQUAL 0) + set(ENV{GIT_COMMIT_SUBJECT} "${output}") + endif() + + RunGitCommand(show -s "--format=%b" ${object}) + if(exit_code EQUAL 0) + if(output) + # Escape line breaks in the commit message. + string(REPLACE "\r\n" "\\r\\n\\\r\n" safe ${output}) + if(safe STREQUAL output) + # Didn't have windows lines - try unix lines. + string(REPLACE "\n" "\\n\\\n" safe ${output}) + endif() + else() + # There was no commit body - set the safe string to empty. + set(safe "") + endif() + set(ENV{GIT_COMMIT_BODY} "\"${safe}\"") + else() + set(ENV{GIT_COMMIT_BODY} "\"\"") # empty string. + endif() + + # >>> + # 2. Additional git properties can be added here via the + # "execute_process()" command. Be sure to set them in + # the environment using the same variable name you added + # to the "_state_variable_names" list. + + if(EXISTS "${GIT_WORKING_DIR}/VERSION") + file(READ "${GIT_WORKING_DIR}/VERSION" version_output_raw) + string(STRIP "${version_output_raw}" version_output) + set(ENV{VERSION_STRING} "${version_output}") + else() + set(ENV{VERSION_STRING} "") + endif() + +endfunction() + + + +# Function: GitStateChangedAction +# Description: this function is executed when the state of the git +# repository changes (e.g. a commit is made). +function(GitStateChangedAction) + foreach(var_name ${_state_variable_names}) + set(${var_name} $ENV{${var_name}}) + endforeach() + configure_file("${PRE_CONFIGURE_FILE}" "${POST_CONFIGURE_FILE}" @ONLY) +endfunction() + + + +# Function: HashGitState +# Description: loop through the git state variables and compute a unique hash. +# Args: +# _state (out) string; a hash computed from the current git state. +function(HashGitState _state) + set(ans "") + foreach(var_name ${_state_variable_names}) + string(SHA256 ans "${ans}$ENV{${var_name}}") + endforeach() + set(${_state} ${ans} PARENT_SCOPE) +endfunction() + + + +# Function: CheckGit +# Description: check if the git repo has changed. If so, update the state file. +# Args: +# _working_dir (in) string; the directory from which git commands will be ran. +# _state_changed (out) bool; whether or no the state of the repo has changed. +function(CheckGit _working_dir _state_changed) + + # Get the current state of the repo. + GetGitState("${_working_dir}") + + # Convert that state into a hash that we can compare against + # the hash stored on-disk. + HashGitState(state) + + # Issue 14: post-configure file isn't being regenerated. + # + # Update the state to include the SHA256 for the pre-configure file. + # This forces the post-configure file to be regenerated if the + # pre-configure file has changed. + file(SHA256 ${PRE_CONFIGURE_FILE} preconfig_hash) + string(SHA256 state "${preconfig_hash}${state}") + + # Check if the state has changed compared to the backup on disk. + if(EXISTS "${GIT_STATE_FILE}") + file(READ "${GIT_STATE_FILE}" OLD_HEAD_CONTENTS) + if(OLD_HEAD_CONTENTS STREQUAL "${state}") + # State didn't change. + set(${_state_changed} "false" PARENT_SCOPE) + return() + endif() + endif() + + # The state has changed. + # We need to update the state file on disk. + # Future builds will compare their state to this file. + file(WRITE "${GIT_STATE_FILE}" "${state}") + set(${_state_changed} "true" PARENT_SCOPE) +endfunction() + + + +# Function: SetupGitMonitoring +# Description: this function sets up custom commands that make the build system +# check the state of git before every build. If the state has +# changed, then a file is configured. +function(SetupGitMonitoring) + add_custom_target(check_git + ALL + DEPENDS ${PRE_CONFIGURE_FILE} + BYPRODUCTS + ${POST_CONFIGURE_FILE} + ${GIT_STATE_FILE} + COMMENT "Checking the git repository for changes..." + COMMAND + ${CMAKE_COMMAND} + -D_BUILD_TIME_CHECK_GIT=TRUE + -DGIT_WORKING_DIR=${GIT_WORKING_DIR} + -DGIT_EXECUTABLE=${GIT_EXECUTABLE} + -DGIT_STATE_FILE=${GIT_STATE_FILE} + -DPRE_CONFIGURE_FILE=${PRE_CONFIGURE_FILE} + -DPOST_CONFIGURE_FILE=${POST_CONFIGURE_FILE} + -P "${CMAKE_CURRENT_LIST_FILE}") +endfunction() + + + +# Function: Main +# Description: primary entry-point to the script. Functions are selected based +# on whether it's configure or build time. +function(Main) + if(_BUILD_TIME_CHECK_GIT) + # Check if the repo has changed. + # If so, run the change action. + CheckGit("${GIT_WORKING_DIR}" changed) + if(changed OR NOT EXISTS "${POST_CONFIGURE_FILE}") + GitStateChangedAction() + endif() + else() + # >> Executes at configure time. + SetupGitMonitoring() + endif() +endfunction() + +# And off we go... +Main() diff --git a/mcsema/Version.h.in b/mcsema/Version.h.in deleted file mode 100644 index 522446e16..000000000 --- a/mcsema/Version.h.in +++ /dev/null @@ -1,19 +0,0 @@ -/* - * Copyright (c) 2020 Trail of Bits, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -#define MCSEMA_VERSION_STRING "@MCSEMA_VERSION_STRING@" -#define MCSEMA_BRANCH_NAME "@MCSEMA_BRANCH_NAME@" diff --git a/mcsema/Version/CMakeLists.txt b/mcsema/Version/CMakeLists.txt new file mode 100644 index 000000000..2554b4259 --- /dev/null +++ b/mcsema/Version/CMakeLists.txt @@ -0,0 +1,16 @@ +# Based on: https://github.com/andrew-hardin/cmake-git-version-tracking/blob/master/better-example/CMakeLists.txt +# By Andrew Hardin +# Released under the MIT License. +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/master/LICENSE +# +# Define the two required variables before including +# the source code for watching a git repository. +set(PRE_CONFIGURE_FILE "Version.cpp.in") +set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/Version.cpp") +set(GIT_WORKING_DIR "${MCSEMA_SOURCE_DIR}") +include("${MCSEMA_SOURCE_DIR}/cmake/git_watcher.cmake") + +# Create a library out of the compiled post-configure file. +add_library(McSemaVersion STATIC ${POST_CONFIGURE_FILE}) +target_include_directories(McSemaVersion PUBLIC ${MCSEMA_SOURCE_DIR}) +add_dependencies(McSemaVersion check_git) diff --git a/mcsema/Version/Version.cpp.in b/mcsema/Version/Version.cpp.in new file mode 100644 index 000000000..e91333479 --- /dev/null +++ b/mcsema/Version/Version.cpp.in @@ -0,0 +1,61 @@ +// Copyright (C) 2020 Trail of Bits +// Based on: https://github.com/andrew-hardin/cmake-git-version-tracking/blob/master/better-example/git.cc.in +// Which is (C) 2020 Andrew Hardin +// +// MIT License +// Copyright (c) 2020 Andrew Hardin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +#include "mcsema/Version/Version.h" + +namespace mcsema { +namespace Version { + + bool HasVersionData() { + return @GIT_RETRIEVED_STATE@; + } + bool HasUncommittedChanges() { + return @GIT_IS_DIRTY@; + } + std::string GetAuthorName() { + return "@GIT_AUTHOR_NAME@"; + } + std::string GetAuthorEmail() { + return "@GIT_AUTHOR_EMAIL@"; + } + std::string GetCommitHash() { + return "@GIT_HEAD_SHA1@"; + } + std::string GetCommitDate() { + return "@GIT_COMMIT_DATE_ISO8601@"; + } + std::string GetCommitSubject() { + return "@GIT_COMMIT_SUBJECT@"; + } + std::string GetCommitBody() { + return @GIT_COMMIT_BODY@; + } + std::string GetVersionString() { + return "@VERSION_STRING@"; + } + + +} // namespace Version +} // namespace mcsema diff --git a/mcsema/Version/Version.h b/mcsema/Version/Version.h new file mode 100644 index 000000000..d4e481cda --- /dev/null +++ b/mcsema/Version/Version.h @@ -0,0 +1,44 @@ +#pragma once +// Copyright (C) 2020 Trail of Bits +// Based on: https://github.com/andrew-hardin/cmake-git-version-tracking/blob/master/better-example/git.h +// Which is (C) 2020 Andrew Hardin +// +// MIT License +// Copyright (c) 2020 Andrew Hardin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// + +#include + +namespace mcsema { +namespace Version { + +bool HasVersionData(); +bool HasUncommittedChanges(); +std::string GetAuthorName(); +std::string GetAuthorEmail(); +std::string GetCommitHash(); +std::string GetCommitDate(); +std::string GetCommitSubject(); +std::string GetCommitBody(); +std::string GetVersionString(); + +} // namespace Version +} // namespace mcsema \ No newline at end of file diff --git a/tools/mcsema_lift/Lift.cpp b/tools/mcsema_lift/Lift.cpp index 5e2617fee..cee47a84c 100644 --- a/tools/mcsema_lift/Lift.cpp +++ b/tools/mcsema_lift/Lift.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -42,14 +43,6 @@ # define LLVM_VERSION_STRING LLVM_VERSION_MAJOR << "." << LLVM_VERSION_MINOR #endif -#ifndef MCSEMA_VERSION_STRING -# define MCSEMA_VERSION_STRING "unknown" -#endif // MCSEMA_VERSION_STRING - -#ifndef MCSEMA_BRANCH_NAME -# define MCSEMA_BRANCH_NAME "unknown" -#endif // MCSEMA_BRANCH_NAME - DECLARE_string(arch); DECLARE_string(os); @@ -82,11 +75,29 @@ DEFINE_bool(legacy_mode, false, namespace { -static void PrintVersion(void) { - std::cout << "This is mcsema-lift version: " << MCSEMA_VERSION_STRING - << std::endl - << "Built from branch: " << MCSEMA_BRANCH_NAME << std::endl - << "Using LLVM " << LLVM_VERSION_STRING << std::endl; +static void SetVersion(void) { + std::stringstream ss; + auto vs = mcsema::Version::GetVersionString(); + if (0 == vs.size()) { + vs = "unknown"; + } + ss << vs << "\n"; + if (!mcsema::Version::HasVersionData()) { + ss << "No extended version information found!\n"; + } else { + ss << "Commit Hash: " << mcsema::Version::GetCommitHash() << "\n"; + ss << "Commit Date: " << mcsema::Version::GetCommitDate() << "\n"; + ss << "Last commit by: " << mcsema::Version::GetAuthorName() << " [" + << mcsema::Version::GetAuthorEmail() << "]\n"; + ss << "Commit Subject: [" << mcsema::Version::GetCommitSubject() << "]\n"; + ss << "\n"; + if (mcsema::Version::HasUncommittedChanges()) { + ss << "Uncommitted changes were present during build.\n"; + } else { + ss << "All changes were committed prior to building.\n"; + } + } + google::SetVersionString(ss.str()); } // Print a list of instructions that Remill can lift. @@ -643,6 +654,8 @@ int main(int argc, char *argv[]) { llvm::cl::ParseCommandLineOptions(1, llvm_argv); google::SetUsageMessage(ss.str()); + + SetVersion(); google::ParseCommandLineFlags(&argc, &argv, true); if (FLAGS_log.empty()) { @@ -653,11 +666,6 @@ int main(int argc, char *argv[]) { FLAGS_minloglevel = FLAGS_loglevel; - if (FLAGS_version) { - PrintVersion(); - return EXIT_SUCCESS; - } - if (FLAGS_os.empty() || FLAGS_arch.empty() || FLAGS_cfg.empty()) { std::cout << google::ProgramUsage() << std::endl; return EXIT_FAILURE;