diff --git a/CMakeLists.txt b/CMakeLists.txt index ff493cd..0bebe02 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -34,6 +34,7 @@ endif() set(public_include_header_list "${PROJECT_SOURCE_DIR}/include/libsleigh.hh" "${PROJECT_SOURCE_DIR}/support/Support.h" + "${PROJECT_SOURCE_DIR}/support/Version.h" "${library_root}/address.hh" "${library_root}/context.hh" "${library_root}/emulate.hh" diff --git a/cmake/git_watcher.cmake b/cmake/git_watcher.cmake new file mode 100644 index 0000000..3da5a80 --- /dev/null +++ b/cmake/git_watcher.cmake @@ -0,0 +1,350 @@ +# git_watcher.cmake +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/98441f9dc056b8d3d4b4287b09e6a99775d70305/git_watcher.cmake +# +# Released under the MIT License. +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/98441f9dc056b8d3d4b4287b09e6a99775d70305/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. +# +# GIT_FAIL_IF_NONZERO_EXIT (OPTIONAL) +# -- Raise a FATAL_ERROR if any of the git commands return a non-zero +# exit code. This is set to TRUE by default. You can set this to FALSE +# if you'd like the build to continue even if a git command fails. +# +# GIT_IGNORE_UNTRACKED (OPTIONAL) +# -- Ignore the presence of untracked files when detecting if the +# working tree is dirty. This is set to FALSE by default. +# +# 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_NOPATH var_name default_value) + if(NOT DEFINED ${var_name}) + set(${var_name} ${default_value}) + endif() +endmacro() + +# Check that an optional variable is set, or, set it to a default value. +# Also converts that path to an abspath. +macro(CHECK_OPTIONAL_VARIABLE var_name default_value) + CHECK_OPTIONAL_VARIABLE_NOPATH(${var_name} ${default_value}) + 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_OPTIONAL_VARIABLE_NOPATH(GIT_FAIL_IF_NONZERO_EXIT TRUE) +CHECK_OPTIONAL_VARIABLE_NOPATH(GIT_IGNORE_UNTRACKED FALSE) + +# 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 + GIT_DESCRIBE + # >>> + # 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_VARIABLE stderr + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_RETRIEVED_STATE} "false") + + # Issue 26: git info not properly set + # + # Check if we should fail if any of the exit codes are non-zero. + if(GIT_FAIL_IF_NONZERO_EXIT) + string(REPLACE ";" " " args_with_spaces "${ARGV}") + message(FATAL_ERROR "${stderr} (${GIT_EXECUTABLE} ${args_with_spaces})") + endif() + 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. + if (GIT_IGNORE_UNTRACKED) + set(untracked_flag "-uno") + else() + set(untracked_flag "-unormal") + endif() + RunGitCommand(status --porcelain ${untracked_flag}) + 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) + # Escape \ + string(REPLACE "\\" "\\\\" output "${output}") + # Escape quotes + string(REPLACE "\"" "\\\"" output "${output}") + set(ENV{GIT_COMMIT_SUBJECT} "${output}") + endif() + + RunGitCommand(show -s "--format=%b" ${object}) + if(exit_code EQUAL 0) + if(output) + # Escape \ + string(REPLACE "\\" "\\\\" output "${output}") + # Escape quotes + string(REPLACE "\"" "\\\"" output "${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() + + # Get output of git describe + RunGitCommand(describe --always ${object}) + if(NOT exit_code EQUAL 0) + set(ENV{GIT_DESCRIBE} "unknown") + else() + set(ENV{GIT_DESCRIBE} "${output}") + 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. + +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} + -DGIT_FAIL_IF_NONZERO_EXIT=${GIT_FAIL_IF_NONZERO_EXIT} + -DGIT_IGNORE_UNTRACKED=${GIT_IGNORE_UNTRACKED} + -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/include/libsleigh.hh b/include/libsleigh.hh index 2ab9a97..8631cad 100644 --- a/include/libsleigh.hh +++ b/include/libsleigh.hh @@ -36,3 +36,4 @@ #pragma GCC diagnostic pop #include "Support.h" +#include "Version.h" diff --git a/support/CMakeLists.txt b/support/CMakeLists.txt index 20c946b..17fd7a1 100644 --- a/support/CMakeLists.txt +++ b/support/CMakeLists.txt @@ -13,8 +13,29 @@ configure_file( @ONLY ) +# Based on: https://github.com/andrew-hardin/cmake-git-version-tracking/blob/98441f9dc056b8d3d4b4287b09e6a99775d70305/better-example/CMakeLists.txt +# By Andrew Hardin +# Released under the MIT License. +# https://raw.githubusercontent.com/andrew-hardin/cmake-git-version-tracking/98441f9dc056b8d3d4b4287b09e6a99775d70305/LICENSE +# +# Define the two required variables before including +# the source code for watching a git repository. +set(PRE_CONFIGURE_FILE "${CMAKE_CURRENT_LIST_DIR}/Version.cpp.in") +set(POST_CONFIGURE_FILE "${CMAKE_CURRENT_BINARY_DIR}/Version.cpp") +set(GIT_FAIL_IF_NONZERO_EXIT OFF) +include("${PROJECT_SOURCE_DIR}/cmake/git_watcher.cmake") + +# Generate GHIDRA version helpers +configure_file( + "${CMAKE_CURRENT_LIST_DIR}/GhidraVersion.cpp.in" + "${CMAKE_CURRENT_BINARY_DIR}/GhidraVersion.cpp" + @ONLY +) + set(sleigh_support_source_list Support.cpp + ${POST_CONFIGURE_FILE} + "${CMAKE_CURRENT_BINARY_DIR}/GhidraVersion.cpp" ) add_library(support @@ -26,4 +47,7 @@ add_library(sleigh::support ALIAS support) target_link_libraries(support PUBLIC sleigh::sleigh_settings ) -target_include_directories(support PRIVATE "$") +target_include_directories(support PRIVATE + "$" + "$" +) diff --git a/support/GhidraVersion.cpp.in b/support/GhidraVersion.cpp.in new file mode 100644 index 0000000..2a0c387 --- /dev/null +++ b/support/GhidraVersion.cpp.in @@ -0,0 +1,21 @@ +/* + Copyright (c) 2022-present, Trail of Bits, Inc. + All rights reserved. + + This source code is licensed in accordance with the terms specified in + the LICENSE file found in the root directory of this source tree. +*/ + +#include "Version.h" + +namespace sleigh { + +std::string_view GetGhidraVersion(void) { return "@ghidra_version@"; } + +std::string_view GetGhidraCommitHash(void) { return "@ghidra_git_tag@"; } + +std::string_view GetGhidraReleaseType(void) { + return "@SLEIGH_GHIDRA_RELEASE_TYPE@"; +} + +} // namespace sleigh diff --git a/support/Version.cpp.in b/support/Version.cpp.in new file mode 100644 index 0000000..26671b5 --- /dev/null +++ b/support/Version.cpp.in @@ -0,0 +1,54 @@ +/* + Copyright (c) 2022-present, Trail of Bits, Inc. + All rights reserved. + + This source code is licensed in accordance with the terms specified in + the LICENSE file found in the root directory of this source tree. + + Based on: + https://github.com/andrew-hardin/cmake-git-version-tracking/blob/98441f9dc056b8d3d4b4287b09e6a99775d70305/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 "Version.h" + +namespace sleigh { + +bool HasVersionData(void) { return @GIT_RETRIEVED_STATE@; } + +bool HasUncommittedChanges(void) { return @GIT_IS_DIRTY@; } + +std::string_view GetAuthorName(void) { return "@GIT_AUTHOR_NAME@"; } + +std::string_view GetAuthorEmail(void) { return "@GIT_AUTHOR_EMAIL@"; } + +std::string_view GetCommitHash(void) { return "@GIT_HEAD_SHA1@"; } + +std::string_view GetCommitDate(void) { return "@GIT_COMMIT_DATE_ISO8601@"; } + +std::string_view GetCommitSubject(void) { return "@GIT_COMMIT_SUBJECT@"; } + +std::string_view GetCommitBody(void) { return "@GIT_COMMIT_BODY@"; } + +} // namespace sleigh diff --git a/support/Version.h b/support/Version.h new file mode 100644 index 0000000..c3d70b7 --- /dev/null +++ b/support/Version.h @@ -0,0 +1,37 @@ +/* + Copyright (c) 2022-present, Trail of Bits, Inc. + All rights reserved. + + This source code is licensed in accordance with the terms specified in + the LICENSE file found in the root directory of this source tree. +*/ + +#pragma once + +#include + +namespace sleigh { + +bool HasVersionData(void); + +bool HasUncommittedChanges(void); + +std::string_view GetAuthorName(void); + +std::string_view GetAuthorEmail(void); + +std::string_view GetCommitHash(void); + +std::string_view GetCommitDate(void); + +std::string_view GetCommitSubject(void); + +std::string_view GetCommitBody(void); + +std::string_view GetGhidraVersion(void); + +std::string_view GetGhidraCommitHash(void); + +std::string_view GetGhidraReleaseType(void); + +} // namespace sleigh diff --git a/tools/sleigh-lift/src/main.cpp b/tools/sleigh-lift/src/main.cpp index 66bb380..24b407c 100644 --- a/tools/sleigh-lift/src/main.cpp +++ b/tools/sleigh-lift/src/main.cpp @@ -12,10 +12,37 @@ #include #include -static void PrintUsage(void) { - std::cerr << "Usage: sleigh-lift [action] [sla_file] [bytes] [-a address] " - "[-p root_sla_dir] [-s pspec_file]" - << std::endl; +static void PrintUsage(std::ostream &os) { + os << "Usage: sleigh-lift [action] [sla_file] [bytes] [-a address] " + "[-p root_sla_dir] [-s pspec_file]" + << std::endl; +} + +static void PrintVersion(void) { + std::cout << "sleigh-lift " << sleigh::GetGhidraVersion() << '\n'; + + // Print out the commit info for the underlying GHIDRA checkout + std::cout << "GHIDRA Version: " << sleigh::GetGhidraVersion() << '\n' + << "GHIDRA Commit Hash: " << sleigh::GetGhidraCommitHash() << '\n' + << "GHIDRA Release Type: " << sleigh::GetGhidraReleaseType() + << '\n'; + + // Now print out the Git commit information + if (sleigh::HasVersionData()) { + std::cout << "Commit Hash: " << sleigh::GetCommitHash() << '\n' + << "Commit Date: " << sleigh::GetCommitDate() << '\n' + << "Last commit by: " << sleigh::GetAuthorName() << " [" + << sleigh::GetAuthorEmail() << "]\n" + << "Commit Subject: [" << sleigh::GetCommitSubject() << "]\n" + << '\n'; + if (sleigh::HasUncommittedChanges()) { + std::cout << "Uncommitted changes were present during build.\n"; + } else { + std::cout << "All changes were committed prior to building.\n"; + } + } else { + std::cout << "No extended version information found!\n"; + } } class InMemoryLoadImage : public LoadImage { @@ -202,9 +229,20 @@ std::optional ParseArgs(int argc, char *argv[]) { } int main(int argc, char *argv[]) { + // Check for `--help` or `--version` + if (argc == 2) { + const std::string cmd = argv[1]; + if (cmd == "--help") { + PrintUsage(std::cout); + return EXIT_SUCCESS; + } else if (cmd == "--version") { + PrintVersion(); + return EXIT_SUCCESS; + } + } const auto args = ParseArgs(argc, argv); if (!args) { - PrintUsage(); + PrintUsage(std::cerr); return EXIT_FAILURE; } const uint64_t addr = args->addr ? *args->addr : 0;