Squash improve-ide-compat into main

Fixes GH-5

- cppcheck replaced with clang-tidy
- clang-tidy lint warnings fixed
- Reworked build tools from scratch to make IDE setup easier
- Added 1.5 IDE setup guides
This commit is contained in:
2023-11-10 21:30:55 +01:00
parent 189d19fd4a
commit ae4e265a90
66 changed files with 2017 additions and 1498 deletions

View File

@@ -1,66 +0,0 @@
#!/bin/false
# Writes a message to stderr.
# Parameters:
# $@ - the message to display
function error() {
echo >&2 "`basename "$0"`: $@"
}
# Writes a message to stderr and exits with code 1.
# Parameters:
# $@ - the message to display
function fail() {
error "$@"
exit 1;
}
# Ensures that a variable with name $1 has a valid executable. If it does not,
# this function attempts to find an executable with a name suggested in $2...$n.
# In either way, if the variable does not end up naming an executable, fail() is
# called.
# Parameters:
# $1 - name of the variable to check and modify
# $2...$n - suggested executables (at least one)
# $FAIL_SILENTLY - if set, don't call exit and don't print anything on failure
function find_cmd() {
declare -n target="$1"
if [ -z "${target+x}" ]; then
for candidate in "${@:2}"; do
if command -v "$candidate" >/dev/null; then
target="$candidate"
break
fi
done
fi
if ! command -v "$target" >/dev/null; then
[ -n "${FAIL_SILENTLY+x}" ] && return 1
fail "Command $2 is not available. Check \$PATH or set \$$1."
fi
unset -n target
return 0
}
# Displays the command and then runs it.
# Parameters:
# $@ - the command to run
function echo_and_run() {
echo " > $*"
command "$@"
}
root_dir="$(dirname "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")"
source_dir="$root_dir"
build_dir="$root_dir/build"
tools_dir="$root_dir/tools"
# Load private.sh
private_sh="$tools_dir/private.sh"
if [ -f "$private_sh" ]; then
[ -x "$private_sh" ] \
|| fail 'tools/private.sh exists but it is not executable'
source "$private_sh"
fi

View File

@@ -1,199 +0,0 @@
#!/bin/bash
usage=\
"Usage: build.sh [OPTIONS...]
Build and run the game.
Options:
--debug make a debug build (default)
--release make a release build
--build-id=ID set the build ID. Default is dev.
--cmake-gen=ARGS pass additional arguments to pass to cmake when
generating build files. ARGS is the ;-separated list.
--dont-generate don't generate build instructions; use existing
configuration if building
--dont-build don't build; run existing binaries or generate build
instructions only
--debug-vulkan enable Vulkan validation layers from LunarG
-R, --run run the game after building
--memcheck[=ARGS] run the game using valgrind's memcheck dynamic memory
analysis tool. Implies -R. ARGS is the ;-separated
list of arguments to pass to valgrind/memcheck.
-h, --help display this help and exit
Environment variables:
PARALLELISM threads to use, default is 1
CMAKE cmake executable
VALGRIND valgrind executable
private.sh variables:
private_cmake_gen_args array of additional arguments to pass to cmake when
generating build files
See also: tools/cppcheck/use-cppcheck.sh --help
tools/clang-format/use-clang-format.sh --help
tools/setup.sh --help"
rsrc="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
source "$rsrc/bashlib.sh"
# Parse arguments
build_type=Debug
do_generate=true
cmake_gen_args=()
do_build=true
run_type=Normal
do_run=''
debug_vulkan=''
memcheck_args=()
for arg in "$@"; do
case "$arg" in
-h | --help )
echo "$usage"
exit
;;
--debug )
build_type=Debug
;;
--release )
build_type=Release
;;
--build-id )
fail "Option --build-id=ID requires a parameter"
;;
--build-id=* )
build_id="${arg#*=}"
;;
--cmake-gen )
fail "Option --cmake-gen=ARGS requires a parameter"
;;
--cmake-gen=* )
readarray -t -d ';' new_cmake_gen_args <<<"${arg#*=};"
unset new_cmake_gen_args[-1]
cmake_gen_args+=("${new_cmake_gen_args[@]}")
unset new_cmake_gen_args
;;
--debug-vulkan )
debug_vulkan=true
;;
-R | --run )
do_run=true
;;
--memcheck )
do_run=true
run_type=memcheck
;;
--memcheck=* )
do_run=true
run_type=memcheck
readarray -t -d ';' new_memcheck_args <<<"${arg#*=};"
unset new_memcheck_args[-1]
memcheck_args+=("${new_memcheck_args[@]}")
unset new_memcheck_args
;;
--dont-generate )
do_generate=''
;;
--dont-build )
do_build=''
;;
* )
fail "Unknown option '$arg'"
;;
esac
done
if [ -z "$do_build" -a -z "$do_generate" -a ${#cmake_gen_args[@]} != 0 ]; then
fail "CMake arguments are set, but no build is requested. Aborting"
fi
if [ -z "$do_build" -a -z "$do_generate" -a -z "$do_run" ]; then
fail "Nothing to do"
fi
# Generate build files
find_cmd CMAKE cmake
if [ $do_generate ]; then
cmake_gen_managed_args=(
-DCMAKE_BUILD_TYPE=$build_type
-DVULKAN_ERROR_CHECKING=`[ $debug_vulkan ] && echo ON || echo OFF`
-UBUILD_ID
)
[ -n "${build_id+x}" ] && cmake_gen_managed_args+=(
-DBUILD_ID="$build_id"
)
echo_and_run "$CMAKE" \
-B "$build_dir" \
-S "$source_dir" \
"${cmake_gen_managed_args[@]}" \
"${private_cmake_gen_args[@]}" \
"${cmake_gen_args[@]}" \
|| fail "Could not generate build files"
fi
# Build
find_cmd CMAKE cmake
if [ $do_build ]; then
options=()
[ -n "${PARALLELISM+x}" ] && options+=(-j "$PARALLELISM")
echo_and_run "$CMAKE" \
--build "$build_dir" \
"${options[@]}" \
|| fail "Build failed"
unset options
fi
# Run
if [ $do_run ]; then
run_command=()
if [ $run_type == memcheck ]; then
find_cmd VALGRIND valgrind
run_command+=(
"$VALGRIND"
--tool=memcheck
--suppressions="$tools_dir"/memcheck/suppressions.supp
"${memcheck_args[@]}"
--
)
fi
run_command+=(
"$build_dir/progressia"
)
run_dir="$root_dir/run"
mkdir -p "$run_dir"
(
cd "$run_dir"
echo_and_run "${run_command[@]}"
echo "Process exited with code $?"
)
fi

View File

@@ -1,4 +0,0 @@
BasedOnStyle: LLVM
# Use larger indentation
IndentWidth: 4

View File

@@ -1,102 +0,0 @@
#!/bin/bash
usage=\
"Usage: use-clang-format.sh git
or: use-clang-format.sh files FILES...
or: use-clang-format.sh raw ARGUMENTS...
In the 1st form, format all files that have changed since last git commit.
In the 2nd form, format all FILES, treating directories recursively.
In the 3rd form, run \`clang-format --style=<style> ARGUMENTS...\`.
Environment variables:
CLANG_FORMAT clang-format executable
CLANG_FORMAT_DIFF clang-format-diff script"
rsrc="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
source "$rsrc/../bashlib.sh"
case "$1" in
git )
find_cmd CLANG_FORMAT_DIFF \
clang-format-diff-13 \
clang-format-diff \
clang-format-diff.py
;;
files | raw )
find_cmd CLANG_FORMAT \
clang-format-13 \
clang-format
;;
-h | --help | '' )
echo "$usage"
exit
;;
* )
fail "Unknown option '$1'"
;;
esac
# Generate style argument
style=''
while IFS='' read line; do
[ -z "$line" ] && continue
[ "${line:0:1}" = '#' ] && continue
[ -n "$style" ] && style+=', '
style+="$line"
done < "$rsrc/clang-format.yml"
style="{$style}" # Not typo
case "$1" in
git )
unstaged_changes="`git diff --name-only`"
if [ -n "$unstaged_changes" ]; then
fail "Refusing to operate in git repository with unstaged changes:
$unstaged_changes"
fi
git diff -U0 --no-color --relative HEAD \
{desktop,main}/{'*.cpp','*.h','*.inl'} \
| command "$CLANG_FORMAT_DIFF" -p1 -style="$style" -i --verbose
exit_code="$?"
git add "$root_dir"
exit "$exit_code"
;;
raw )
command "$CLANG_FORMAT" -style="$style" "$@"
;;
files )
files=()
for input in "${@:2}"; do
if [ -d "$input" ]; then
readarray -d '' current_files < <(
find "$input" \
\( -name '*.cpp' -o -name '*.h' -o -name '*.inl' \) \
-type f \
-print0 \
)
[ "${#current_files[@]}" -eq 0 ] \
&& fail "No suitable files found in directory $input"
files+=("${current_files[@]}")
else
case "$input" in
*.cpp | *.h | *.inl )
files+=("$input")
;;
* )
error "Refusing to format file '$input': `
`only .cpp, .h and .inl supported"
;;
esac
fi
done
[ "${#files[@]}" -eq 0 ] && fail "No files to format"
command "$CLANG_FORMAT" -style="$style" -i --verbose "${files[@]}"
;;
esac

View File

@@ -1,56 +0,0 @@
# Global variables. Yikes. FIXME
set(tools ${PROJECT_SOURCE_DIR}/tools)
set(generated ${PROJECT_BINARY_DIR}/generated)
set(assets_to_embed "")
set(assets_to_embed_args "")
file(MAKE_DIRECTORY ${generated})
find_package(Vulkan COMPONENTS glslc REQUIRED)
find_program(glslc_executable NAMES glslc HINTS Vulkan::glslc)
set(shaders ${generated}/shaders)
file(MAKE_DIRECTORY ${shaders})
# Shedules compilation of shaders
# Adapted from https://stackoverflow.com/a/60472877/4463352
macro(compile_shader)
foreach(source ${ARGV})
get_filename_component(source_basename ${source} NAME)
set(tmp "${shaders}/${source_basename}.spv")
add_custom_command(
OUTPUT ${tmp}
DEPENDS ${source}
COMMAND ${glslc_executable}
-o ${tmp}
${CMAKE_CURRENT_SOURCE_DIR}/${source}
COMMENT "Compiling shader ${source}"
)
list(APPEND assets_to_embed_args "${tmp};as;${source_basename}.spv")
list(APPEND assets_to_embed "${tmp}")
unset(tmp)
unset(source_basename)
endforeach()
endmacro()
compile_shader(
desktop/graphics/shaders/shader.frag
desktop/graphics/shaders/shader.vert
)
# Generate embed files
add_custom_command(
OUTPUT ${generated}/embedded_resources.cpp
${generated}/embedded_resources.h
COMMAND ${tools}/embed/embed.py
--cpp ${generated}/embedded_resources.cpp
--header ${generated}/embedded_resources.h
--
${assets_to_embed_args}
DEPENDS "${assets_to_embed}"
${tools}/embed/embed.py
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMENT "Embedding assets"
)

View File

@@ -1,28 +0,0 @@
# CppCheck command line arguments
# Each line is treated as one argument, unless it is empty or it starts with #.
#
# Available variables:
# ${CMAKE_SOURCE_DIR} project root
# ${CMAKE_BINARY_DIR} CMake build directory
--enable=warning,style,information
#--enable=unusedFunction # Unused functions are often OK since they are intended
# # to be used later
#--enable=missingInclude # Very prone to false positives; system-dependent
--inconclusive
# SUPPRESSIONS
# Warnings that are suppressed on a case-by-case basis should be suppressed
# using inline suppressions.
# Warnings that were decided to be generally inapplicable should be suppressed
# using suppressions.txt.
# Warnings that result from the way cppcheck is invoked should be suppressed
# using this file.
--inline-suppr
--suppressions-list=${CMAKE_SOURCE_DIR}/tools/cppcheck/suppressions.txt
# N.B.: this path is also mentioned in use scripts
--cppcheck-build-dir=${CMAKE_BINARY_DIR}/cppcheck
--error-exitcode=2

View File

@@ -1,18 +0,0 @@
# CppCheck global suppressions
# Do not use this file for suppressions that could easily be declared inline.
# Allow the use of implicit constructors.
noExplicitConstructor:*
# In most cases using STL algorithm functions causes unnecessary code bloat.
useStlAlgorithm:*
# cppcheck trips on #include <embedded_resources.h> and there's no way to
# suppress that exlusively
missingInclude:*
# Shut up. Just shut up.
unmatchedSuppression:*
# Do not check third-party libraries
*:*lib*

View File

@@ -1,65 +0,0 @@
#!/bin/bash
usage=\
"Usage: use-cppcheck.sh
Run cppcheck with correct options.
Environment variables:
PARALLELISM threads to use, default is 1
CPPCHECK cppcheck executable
CMAKE cmake executable"
rsrc="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
source "$rsrc/../bashlib.sh"
find_cmd CPPCHECK cppcheck
find_cmd CMAKE cmake
case "$1" in
-h | --help )
echo "$usage"
exit
;;
esac
# Generate compile database for CppCheck
command "$CMAKE" \
-B "$build_dir" \
-S "$source_dir" \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON
compile_database="$build_dir/compile_commands.json"
mkdir -p "$build_dir/cppcheck"
options=()
while IFS='' read -r line; do
[ -z "$line" ] && continue
[ "${line:0:1}" = '#' ] && continue
option="$(
CMAKE_SOURCE_DIR="$source_dir" \
CMAKE_BINARY_DIR="$build_dir" \
envsubst <<<"$line"
)"
options+=("$option")
done < "$tools_dir/cppcheck/options.txt"
[ -n "${PARALLELISM+x}" ] && options+=(-j "$PARALLELISM")
errors="`
echo_and_run "$CPPCHECK" \
--project="$compile_database" \
-D__CPPCHECK__ \
"${options[@]}" \
2>&1 >/dev/fd/0 # Store stderr into variable, pass stdout to our stdout
`"
exit_code="$?"
if [ "$exit_code" -eq 2 ]; then
less - <<<"$errors"
exit "$exit_code"
fi

57
tools/dev-mode.cmake Normal file
View File

@@ -0,0 +1,57 @@
if (DEV_MODE)
find_program(clang_tidy_EXECUTABLE NAMES clang-tidy-13 clang-tidy REQUIRED)
find_package(Python3 COMPONENTS Interpreter REQUIRED)
# Setup clang-tidy
list(APPEND clang_tidy_command "${clang_tidy_EXECUTABLE}"
"--warnings-as-errors=*"
"--use-color")
set_target_properties(progressia
PROPERTIES CXX_CLANG_TIDY "${clang_tidy_command}")
# Display the marker for pre-commit.py at build time
add_custom_target(clang_tidy_marker ALL
COMMAND ${CMAKE_COMMAND} -E echo
"Clang-tidy is enabled. This is a marker for pre-commit.py")
# Notify pre-commit.py about CMake settings
execute_process(COMMAND ${Python3_EXECUTABLE} ${tools}/pre-commit.py
set-build-info -- "${CMAKE_COMMAND}" "${CMAKE_BINARY_DIR}"
RESULT_VARIABLE set_build_info_RESULT)
if(${set_build_info_RESULT})
message(FATAL_ERROR "pre-commit.py set-build-info failed")
endif()
# Setup pre-commit git hook
if (IS_DIRECTORY "${CMAKE_SOURCE_DIR}/.git/hooks")
set(pre_commit_hook "${CMAKE_SOURCE_DIR}/.git/hooks/pre-commit")
if (NOT EXISTS "${pre_commit_hook}")
file(WRITE "${pre_commit_hook}"
"#!/bin/sh\n"
"# Progressia autogenerated pre-commit hook\n"
"# You may modify this hook freely "
"(just make sure the checks run)\n"
"/bin/env python3 ${CMAKE_SOURCE_DIR}/tools/pre-commit.py run")
if (${CMAKE_VERSION} VERSION_LESS "3.19.0")
if (${CMAKE_HOST_UNIX})
execute_process(COMMAND chmod "755" "${pre_commit_hook}"
RESULT_VARIABLE chmod_RESULT)
if (${chmod_RESULT})
message(FATAL_ERROR "Could not make git pre-commit hook executable")
endif()
endif()
else()
file(CHMOD "${pre_commit_hook}"
PERMISSIONS
OWNER_READ OWNER_WRITE OWNER_EXECUTE
GROUP_READ GROUP_EXECUTE
WORLD_READ WORLD_EXECUTE)
endif()
endif()
unset(pre_commit_hook)
endif()
endif()

89
tools/embed/embed.cmake Normal file
View File

@@ -0,0 +1,89 @@
# embed.cmake
# Generates embedded_resources.h and embedded_resources.cpp
find_package(Python3 COMPONENTS Interpreter REQUIRED)
macro (get_target_property_or var target prop default)
get_property(__is_set TARGET ${target} PROPERTY ${prop} SET)
if (__is_set)
get_property(${var} TARGET ${target} PROPERTY ${prop})
else()
set(${var} "${default}")
endif()
unset(__is_set)
endmacro()
function (target_embeds)
set(expecting_name FALSE)
set(target "")
set(current_asset "")
foreach (word ${ARGV})
# First argument is target name
if (target STREQUAL "")
set(target "${word}")
get_target_property_or(script_args "${target}" EMBED_ARGS "")
get_target_property_or(embeds "${target}" EMBEDS "")
continue()
endif()
if (current_asset STREQUAL "")
# Beginning of asset declaration (1/2)
set(current_asset "${word}")
elseif (expecting_name)
# End of "asset AS asset_name"
list(APPEND script_args "${current_asset};as;${word}")
list(APPEND embeds ${current_asset})
set(current_asset "")
set(expecting_name FALSE)
elseif ("${word}" STREQUAL "AS")
# Keyword AS in "asset AS asset_name"
set(expecting_name TRUE)
else()
# End of asset without AS, beginning of asset declaration (2/2)
list(APPEND script_args "${current_asset};as;${current_asset}")
list(APPEND embeds ${current_asset})
set(current_asset "${word}")
endif()
endforeach()
if (expecting_name)
message(FATAL_ERROR "No name given for asset \"${current_asset}\"")
endif()
if (NOT current_asset STREQUAL "")
list(APPEND script_args "${current_asset};as;${current_asset}")
endif()
set_target_properties("${target}" PROPERTIES EMBED_ARGS "${script_args}")
set_target_properties("${target}" PROPERTIES EMBEDS "${embeds}")
endfunction()
file(MAKE_DIRECTORY "${generated}/embedded_resources")
function(compile_embeds target)
get_target_property(script_args "${target}" EMBED_ARGS)
get_target_property(embeds "${target}" EMBEDS)
add_custom_command(
OUTPUT ${generated}/embedded_resources/embedded_resources.cpp
${generated}/embedded_resources/embedded_resources.h
COMMAND ${Python3_EXECUTABLE} ${tools}/embed/embed.py
--cpp ${generated}/embedded_resources/embedded_resources.cpp
--header ${generated}/embedded_resources/embedded_resources.h
--
${script_args}
DEPENDS ${embeds}
${tools}/embed/embed.py
WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
COMMENT "Embedding assets"
)
endfunction()

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python3
usage = \
'''Usage: embed.py --cpp OUT_CPP --header OUT_H [--] [INPUT as PATH]...
'''Usage: %(me)s --cpp OUT_CPP --header OUT_H [--] [INPUT as PATH]...
Generate C++ source code that includes binary contents of INPUT files.
Each file in INPUT is stored as a resource: a static array of unsigned char.
@@ -79,6 +79,7 @@ def main():
fail(f"Unknown option '{arg}'")
elif considerOptions and (arg == '-h' or arg == '--help'):
print(usage % {'me': os.path.basename(sys.argv[0])})
sys.exit(0)
elif considerOptions and arg == '--':
@@ -237,8 +238,8 @@ namespace {
mid=\
'''
std::unordered_map<std::string,
__embedded_resources::EmbeddedResource>
const std::unordered_map<std::string,
__embedded_resources::EmbeddedResource>
EMBEDDED_RESOURCES =
{
''',

View File

@@ -1,51 +0,0 @@
#!/bin/bash
me="$(realpath "${BASH_SOURCE[0]}")"
if [ "$(basename "$me")" = 'pre-commit' ]; then
# i write good shell scripts - Javapony 2022-10-07
root_dir="$(realpath "$(dirname "$me")/../../")"
hook_source="$root_dir/tools/git/hook_pre_commit.sh"
if [ "$hook_source" -nt "$me" ]; then
if [ -n "${ALREADY_UPDATED+x}" ]; then
echo >&2 "git pre-commit hook: Attempted recursive hook update. `
`Something is very wrong."
exit 1
fi
echo ''
echo "===== tools/git/hook_pre_commit.sh updated; `
`replacing pre-commit hook ====="
echo ''
cp "$hook_source" "$me" &&
chmod +x "$me" \
|| fail 'Update failed'
ALREADY_UPDATED=true "$me"
exit $?
fi
source "$root_dir/tools/bashlib.sh"
else
rsrc="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
source "$rsrc/../bashlib.sh"
fi
unstaged_changes="`git diff --name-only`"
if [ -n "$unstaged_changes" ]; then
fail "Please stage all stash all unstaged changes in the following files:
$unstaged_changes"
fi
echo_and_run "$tools_dir/cppcheck/use-cppcheck.sh" \
|| fail "Cppcheck has generated warnings, aborting commit"
echo_and_run "$tools_dir/clang-format/use-clang-format.sh" git \
|| fail "clang-format has failed, aborting commit"
echo_and_run "$tools_dir/build.sh" --dont-generate \
|| fail "Could not build project, aborting commit"
echo 'All checks passed'

53
tools/glslc.cmake Normal file
View File

@@ -0,0 +1,53 @@
# glslc.cmake
# Compiles GLSL shaders to SPV files
find_package(Vulkan COMPONENTS glslc REQUIRED)
find_program(glslc_EXECUTABLE NAMES glslc HINTS Vulkan::glslc REQUIRED)
macro (get_target_property_or var target prop default)
get_property(__is_set TARGET ${target} PROPERTY ${prop} SET)
if (__is_set)
get_property(${var} TARGET ${target} PROPERTY ${prop})
else()
set(${var} "${default}")
endif()
unset(__is_set)
endmacro()
function (target_glsl_shaders)
set(target "")
foreach (word ${ARGV})
# First argument is target name
if (target STREQUAL "")
set(target ${word})
get_target_property_or(glsl_shaders ${target} GLSL_SHADERS "")
else()
list(APPEND glsl_shaders ${word})
endif()
endforeach()
set_target_properties(${target} PROPERTIES GLSL_SHADERS "${glsl_shaders}")
endfunction()
file(MAKE_DIRECTORY "${generated}/compiled_glsl_shaders")
function(compile_glsl target)
get_target_property(glsl_shaders ${target} GLSL_SHADERS)
foreach (source_path ${glsl_shaders})
get_filename_component(source_basename ${source_path} NAME)
set(spv_path
"${generated}/compiled_glsl_shaders/${source_basename}.spv")
add_custom_command(
OUTPUT ${spv_path}
DEPENDS ${source_path}
COMMAND ${glslc_EXECUTABLE}
-o ${spv_path}
${CMAKE_CURRENT_SOURCE_DIR}/${source_path}
COMMENT "Compiling shader ${source_path}"
)
target_embeds(${target} ${spv_path} AS "${source_basename}.spv")
endforeach()
endfunction()

416
tools/pre-commit.py Executable file
View File

@@ -0,0 +1,416 @@
#!/usr/bin/env python3
usage = \
'''Usage: %(me)s run [OPTIONS...]
or: %(me)s restore [OPTIONS...]
or: %(me)s set-build-info CMAKE_EXECUTABLE CMAKE_BINARY_DIR
In the 1st form, run standard pre-commit procedure for Progressia.
In the 2nd form, attempt to restore workspace if the pre-commit hook failed.
In the 3rd form, update cached build settings.
--dry-run do not change anything in git or in the filesystem;
implies --verbose
--verbose print commands and diagnostics
--help display this help and exit
--version display version information and exit
Currently, the pre-commit procedure performs the following:
1. format staged changes
2. attempt to compile with staged changes only
pre-commit-settings.json values:
build-root CMake binary dir to use (filled in by CMake)
parallelism threads to use, default is 1
git git command, default is null
cmake cmake command, default is null (filled in by CMake)
clang-format-diff clang-format-diff command, default is null
Use semicolons to separate arguments in git, cmake and clang-format-diff'''
# Script version. Increment when script logic changes significantly.
# Commit change separately.
version = 1
# Source directories to format
src_dirs = ['desktop', 'main']
# File extensions to format
exts = ['cpp', 'h', 'inl']
import sys
import os
import subprocess
import shutil
import json
STASH_NAME = 'progressia_pre_commit_stash'
# Paths are relative to this script's directory, tools/
SETTINGS_PATH = 'pre-commit-settings.json'
CLANG_TIDY_CHECK_MARKER = 'Clang-tidy is enabled. ' \
'This is a marker for pre-commit.py'
def fail(*args, code=1):
"""Print an error message and exit with given code (default 1)"""
print(my_name + ':', *args, file=sys.stderr)
sys.exit(1)
def verbose(*args):
"""Print a message in verbose mode only."""
if verbose_mode:
print(my_name + ':', *args)
def long_print_iter(title, it):
"""Print contents of iterable titled as specified. If iterable is empty,
print the string (nothing) instead.
"""
print(title + ':')
if len(it) > 0:
print('\t' + '\n\t'.join(it) + '\n')
else:
print('\t(nothing)\n')
def invoke(*cmd, result_when_dry=None, quiet=True, text=True, stdin=None):
"""Execute given system command and return its stdout. If command fails,
throw CalledProcessError.
When in verbose mode, log command before execution. If in dry-run mode and
result_when_dry is not None, skip execution and return result_when_dry
instead.
Keyword arguments:
result_when_dry -- unless None (default), skip execution and return this
quiet -- if False, print stdout (default True)
text -- treat stdin and stdout as text rather than bytes (default False)
stdin -- unless None (default), send this to stdin of spawned process
"""
verbose('command', *(repr(c) for c in cmd))
if dry_run and result_when_dry is not None:
print(my_name + ': skipped: --dry-run')
return result_when_dry
popen = subprocess.Popen(cmd,
stdout=subprocess.PIPE,
text=text,
universal_newlines=text,
stdin=subprocess.PIPE if stdin else subprocess.DEVNULL)
stdout, _ = popen.communicate(input=stdin)
if text and not quiet:
print(stdout, end='')
return_code = popen.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, cmd)
return stdout
def get_file_sets():
"""Return sets of indexed and unindexed files according to Git"""
def git_z(*cmd):
raw = invoke(*git, *cmd, '-z', text=False)
return set(f.decode() for f in raw.split(b'\x00') if len(f) != 0)
indexed = git_z('diff', '--name-only', '--cached')
unindexed = git_z('diff', '--name-only') | \
git_z('ls-files', '--other', '--exclude-standard')
return indexed, unindexed
def run_safety_checks(indexed, unindexed):
if invoke(*git, 'stash', 'list', '--grep', f"\\b{STASH_NAME}$") != '':
fail(f"Cannot run pre-commit checks: stash {STASH_NAME} exists. "
f"Use `{my_name} restore` to restore workspace and repository "
f"state")
both_changes = indexed & unindexed
if len(both_changes) != 0:
fail(f"Cannot run pre-commit checks: files with indexed and unindexed "
'changes exist:\n\n\t' +
'\n\t'.join(both_changes) +
'\n')
def do_restore():
"""Restore repository and filesystem. Fail if stash not found."""
print('Redoing rolled back changes')
git_list = invoke(*git, 'stash', 'list', '--grep', f"\\b{STASH_NAME}$")
if len(git_list) == 0:
if dry_run:
stash_name = 'stash@{0}'
else:
fail(f"Cannot restore repository: stash {STASH_NAME} not found")
else:
stash_name, _, _ = git_list.partition(':')
invoke(*git, 'stash', 'pop', '--index', '--quiet', stash_name,
result_when_dry='', quiet=False)
def format_project():
"""Format staged files with clang-format-diff."""
diff = invoke(*git, 'diff', '-U0', '--no-color', '--relative', 'HEAD',
*(f"{d}/*.{e}" for d in src_dirs for e in exts))
invoke(*clang_format_diff, '-p1', '-i', '--verbose',
stdin=diff, result_when_dry='', quiet=False)
def unformat_project(indexed_existing):
"""Undo formatting changes introduced by format_project()."""
print('Undoing formatting changes')
if len(indexed_existing) == 0:
print('Nothing to do: all indexed changes are deletions')
return
invoke(*git, 'restore', '--', *indexed_existing)
def build_project():
"""Build project with cmake."""
print('Building project')
build_log = invoke(*cmake,
'--build', build_root,
'--parallel', str(parallelism),
result_when_dry=CLANG_TIDY_CHECK_MARKER,
quiet=False)
if CLANG_TIDY_CHECK_MARKER not in build_log.splitlines():
fail('Project build was successful, but clang-tidy did not run. '
'Please make sure DEV_MODE is ON and regenerate CMake cache.')
print('Success')
def pre_commit():
"""Run pre-commit checks."""
if build_root is None:
fail(f"build-root is not set in {SETTINGS_PATH}. Compile project "
'manually to set this variable properly.')
if not os.path.exists(build_root):
fail(f"build-root {build_root} does not exist. Compile project "
'manually to set this variable properly.')
cmakeCache = os.path.join(build_root, 'CMakeCache.txt')
if not os.path.exists(cmakeCache):
fail(f"{cmakeCache} does not exist. build-root is likely invalid. "
'Compile project manually to set this variable properly.')
indexed, unindexed = get_file_sets()
indexed_existing = [f for f in indexed if os.path.exists(f)]
if verbose_mode:
long_print_iter('Indexed changes', indexed)
long_print_iter('Unindexed changes', unindexed)
long_print_iter('Indexed changes without deletions', indexed_existing)
if len(indexed) == 0:
fail('No indexed changes. You probably forgot to run `git add .`')
run_safety_checks(indexed, unindexed)
undo_formatting = False
restore = False
try:
if len(unindexed) != 0:
long_print_iter('Unindexed changes found in files', unindexed)
print('These changes will be rolled back temporarily')
invoke(*git, 'stash', 'push',
'--keep-index',
'--include-untracked',
'--message', STASH_NAME,
result_when_dry='', quiet=False)
restore = True
format_project()
undo_formatting = True
build_project()
undo_formatting = False
finally:
if undo_formatting:
unformat_project(indexed_existing)
if restore:
do_restore()
print('Staging formatting changes')
if len(indexed_existing) == 0:
print('Nothing to do: all indexed changes are deletions')
else:
invoke(*git, 'add', '--', *indexed_existing,
result_when_dry='', quiet=False)
def get_settings_path():
return os.path.abspath(os.path.join(os.path.dirname(__file__),
SETTINGS_PATH))
def save_settings():
"""Save tools/pre-commit-settings.json."""
path = get_settings_path()
verbose(f"Saving settings into {path}")
if not dry_run:
with open(path, mode='w') as f:
json.dump(settings, f, indent=4)
else:
verbose(' skipped: --dry-run')
def set_build_info():
"""Set build info in tools/pre-commit-settings.json."""
settings['build_root'] = arg_build_root
settings['cmake'] = arg_cmake_executable
save_settings()
def parse_args():
"""Parse sys.argv and environment variables; set corresponding globals.
Return (action, arguments for set-build-root).
"""
global action
global verbose_mode
global dry_run
global allow_update
consider_options = True
action = None
arg_cmake_executable = None
arg_build_root = None
for arg in sys.argv[1:]:
if arg == 'restore' or arg == 'set-build-info' or arg == 'run':
if action is not None:
fail(f"Cannot use '{arg}' and '{action}' together")
action = arg
elif consider_options and arg.startswith('-'):
if arg == '-h' or arg == '--help' or arg == 'help' or arg == '/?':
print(usage % {'me': my_name})
sys.exit(0)
elif arg == '--version':
print(f"Progressia pre-commit script, version {version}")
sys.exit(0)
elif arg == '--verbose':
verbose_mode = True
elif arg == '--dry-run':
dry_run = True
verbose_mode = True
elif arg == '--':
consider_options = False
else:
fail(f"Unknown option '{arg}'")
elif action == 'set-build-info' and arg_cmake_executable is None:
arg_cmake_executable = arg
elif action == 'set-build-info' and arg_build_root is None:
arg_build_root = arg
else:
fail(f"Unknown or unexpected argument '{arg}'")
if action is None:
fail('No action specified')
if action == 'set-build-info' and arg_cmake_executable is None:
fail('No CMake executable given')
if action == 'set-build-info' and arg_build_root is None:
fail('No build root given')
return action, arg_build_root, arg_cmake_executable
def load_settings():
"""Ensure pre-commit-settings.json exists and is loaded into memory."""
global settings
path = get_settings_path()
if os.path.exists(path):
with open(path, mode='r') as f:
settings = json.load(f)
else:
verbose(f"{path} not found, using defaults")
settings = {
"__comment": "See `pre-commit.py --help` for documentation",
"build_root": None,
"git": None,
"cmake": None,
"clang_format_diff": None,
"parallelism": 1
}
save_settings()
def parse_settings():
"""Load values from settings and check their validity."""
global settings
global build_root
global git
global cmake
global clang_format_diff
global parallelism
build_root = settings['build_root']
parallelism = settings['parallelism']
def find_command(hints, settings_name):
if settings[settings_name] is not None:
hints = [settings[settings_name]]
cmds = (hint.split(';') for hint in hints)
res = next((cmd for cmd in cmds if shutil.which(cmd[0])), None) \
or fail(f"Command {hints[0]} not found. Set {settings_name} " +
f"in {path} or check PATH")
verbose(f"Found command {hints[0]}:", *(repr(c) for c in res))
return res
git = find_command(['git'], 'git')
cmake = find_command(['cmake'], 'cmake')
clang_format_diff = find_command(['clang-format-diff-13',
'clang-format-diff',
'clang-format-diff.py'],
'clang_format_diff')
if __name__ == '__main__':
my_name = os.path.basename(sys.argv[0])
verbose_mode = False
dry_run = False
allow_update = True
action, arg_build_root, arg_cmake_executable = parse_args()
load_settings()
if dry_run:
print('Running in dry mode: no changes to filesystem or git will '
'actually be performed')
try:
if action == 'set-build-info':
set_build_info()
elif action == 'restore':
parse_settings()
do_restore()
indexed, unindexed = get_file_sets()
if indexed & unindexed:
unformat_project(indexed)
else:
parse_settings()
pre_commit()
except subprocess.CalledProcessError as e:
fail('Command', *(repr(c) for c in e.cmd),
f"exited with code {e.returncode}")

View File

@@ -1,146 +0,0 @@
#!/bin/bash
usage=\
"Usage: setup.sh [--for-development]
Set up the development environment after \`git clone\`
Options:
--for-development perform additional setup only necessary for developers
-h, --help display this help and exit"
rsrc="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
source "$rsrc/bashlib.sh" || {
echo >&2 'Could not load bashlib'
exit 1
}
cd "$root_dir"
# Parse arguments
for_development=''
for arg in "$@"; do
case "$arg" in
-h | --help )
echo "$usage"
exit
;;
--for-development )
for_development=true
;;
* )
fail "Unknown option '$arg'"
;;
esac
done
# Сreate private.sh
if [ ! -e "$private_sh" ]; then
echo '#!/bin/bash
# This file is ignored by git. Use it to configure shell scripts in tools/
# for your development environment.
PARALLELISM=1
#PATH="$PATH:/opt/whatever"
' >"$private_sh" &&
chmod +x "$private_sh" ||
fail "tools/private.sh was not found; could not create it"
echo "Created tools/private.sh"
else
echo "Found and loaded private.sh"
fi
# Check available commands
failed=()
function check_cmd() {
if FAIL_SILENTLY=true find_cmd found "$@"; then
echo "Found command $found"
else
failed+=("command $1")
echo "Could not find command $1"
fi
unset found
}
check_cmd cmake
check_cmd python3
check_cmd glslc
if [ $for_development ]; then
check_cmd git
check_cmd cppcheck
check_cmd clang-format-13 clang-format
check_cmd clang-format-diff-13 clang-format-diff clang-format-diff.py
check_cmd valgrind
fi
# Try generating build files
if FAIL_SILENTLY=true find_cmd CMAKE cmake; then
if CMAKE="$CMAKE" "$tools_dir/build.sh" --dont-build; then
echo 'CMake did not encounter any problems'
else
echo 'Could not generate build files; libraries are probably missing'
failed+=('some libraries, probably (see CMake messages for details)')
fi
else
echo 'Skipping CMake test because cmake was not found'
fi
# Display accumulated errors
[ ${#failed[@]} -ne 0 ] &&
fail "Could not find the following required commands or libraries:
`for f in "${failed[@]}"; do echo " $f"; done`
You can resolve these errors in the following ways:
1. Install required software packages. See README for specific instructions.
2. Edit PATH or CMAKE_MODULE_PATH environment variables in tools/private.sh
to include your installation directories.
"
# Set executable flags
chmod -v +x tools/build.sh \
tools/embed/embed.py \
|| fail 'Could not make scripts executable'
if [ $for_development ]; then
chmod -v +x tools/clang-format/use-clang-format.sh \
tools/cppcheck/use-cppcheck.sh \
|| fail 'Could not make developer scripts executable'
fi
# Set git hook
if [ $for_development ]; then
mkdir -vp .git/hooks &&
cp -v tools/git/hook_pre_commit.sh .git/hooks/pre-commit &&
chmod -v +x .git/hooks/pre-commit \
|| fail 'Could not setup git pre-commit hook'
fi
echo 'Setup complete'