neovim/scripts/pvscheck.sh
2022-11-02 21:45:26 +08:00

496 lines
14 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/sh
# Assume that "local" is available.
# shellcheck disable=SC2039
set -e
# Note: -u causes problems with posh, it barks at “undefined” $@ when no
# arguments provided.
test -z "$POSH_VERSION" && set -u
log_info() {
>&2 printf "pvscheck.sh: %s\n" "$@"
}
get_jobs_num() {
if [ -n "${TRAVIS:-}" ] ; then
# HACK: /proc/cpuinfo on Travis CI is misleading, so hardcode 1.
echo 1
else
echo $(( $(grep -c "^processor" /proc/cpuinfo) + 1 ))
fi
}
help() {
echo 'Usage:'
echo ' pvscheck.sh [--pvs URL] [--deps] [--environment-cc]'
echo ' [target-directory [branch]]'
echo ' pvscheck.sh [--pvs URL] [--recheck] [--environment-cc] [--update]'
echo ' [target-directory]'
echo ' pvscheck.sh [--pvs URL] --only-analyse [target-directory]'
echo ' pvscheck.sh [--pvs URL] --pvs-install {target-directory}'
echo ' pvscheck.sh --patch [--only-build]'
echo
echo ' --pvs: Fetch pvs-studio from URL.'
echo
echo ' --pvs detect: Auto-detect latest version (by scraping viva64.com).'
echo
echo ' --deps: (for regular run) Use top-level Makefile and build deps.'
echo ' Without this it assumes all dependencies are already'
echo ' installed.'
echo
echo ' --environment-cc: (for regular run and --recheck) Do not export'
echo ' CC=clang. Build is still run with CFLAGS=-O0.'
echo
echo ' --only-build: (for --patch) Only patch files in ./build directory.'
echo
echo ' --pvs-install: Only install PVS-studio to the specified location.'
echo
echo ' --patch: patch sources in the current directory.'
echo ' Does not patch already patched files.'
echo ' Does not run analysis.'
echo
echo ' --recheck: run analysis on a prepared target directory.'
echo
echo ' --update: when rechecking first do a pull.'
echo
echo ' --only-analyse: run analysis on a prepared target directory '
echo ' without building Neovim.'
echo
echo ' target-directory: Directory where build should occur.'
echo ' Default: ../neovim-pvs'
echo
echo ' branch: Branch to check.'
echo ' Default: master.'
}
getopts_error() {
local msg="$1" ; shift
local do_help=
if test "$msg" = "--help" ; then
msg="$1" ; shift
do_help=1
fi
printf '%s\n' "$msg" >&2
if test -n "$do_help" ; then
printf '\n' >&2
help >&2
fi
echo 'return 1'
return 1
}
# Usage `eval "$(getopts_long long_defs -- positionals_defs -- "$@")"`
#
# long_defs: list of pairs of arguments like `longopt action`.
# positionals_defs: list of arguments like `action`.
#
# `action` is a space-separated commands:
#
# store_const [const] [varname] [default]
# Store constant [const] (default 1) (note: evaled) if argument is present
# (long options only). Assumes long option accepts no arguments.
# store [varname] [default]
# Store value. Assumes long option needs an argument.
# run {func} [varname] [default]
# Run function {func} and store its output to the [varname]. Assumes no
# arguments accepted (long options only).
# modify {func} [varname] [default]
# Like run, but assumes a single argument, passed to function {func} as $1.
#
# All actions stores empty value if neither [varname] nor [default] are
# present. [default] is evaled by top-level `eval`, so be careful. Also note
# that no arguments may contain spaces, including [default] and [const].
getopts_long() {
local positional=
local opt_bases=""
while test $# -gt 0 ; do
local arg="$1" ; shift
local opt_base=
local act=
local opt_name=
if test -z "$positional" ; then
if test "$arg" = "--" ; then
positional=0
continue
fi
act="$1" ; shift
opt_name="$(echo "$arg" | tr '-' '_')"
opt_base="longopt_$opt_name"
else
if test "$arg" = "--" ; then
break
fi
: $(( positional+=1 ))
act="$arg"
opt_name="arg_$positional"
opt_base="positional_$positional"
fi
opt_bases="$opt_bases $opt_base"
eval "local varname_$opt_base=$opt_name"
local i=0
for act_subarg in $act ; do
eval "local act_$(( i+=1 ))_$opt_base=\"\$act_subarg\""
done
done
# Process options
local positional=0
local force_positional=
while test $# -gt 0 ; do
local argument="$1" ; shift
local opt_base=
local has_equal=
local equal_arg=
local is_positional=
if test "$argument" = "--" ; then
force_positional=1
continue
elif test -z "$force_positional" && test "${argument#--}" != "$argument"
then
local opt_name="${argument#--}"
local opt_name_striparg="${opt_name%%=*}"
if test "$opt_name" = "$opt_name_striparg" ; then
has_equal=0
else
has_equal=1
equal_arg="${argument#*=}"
opt_name="$opt_name_striparg"
fi
# Use trailing x to prevent stripping newlines
opt_name="$(printf '%sx' "$opt_name" | tr '-' '_')"
opt_name="${opt_name%x}"
if test -n "$(printf '%sx' "$opt_name" | tr -d 'a-z_')" ; then
getopts_error "Option contains invalid characters: $opt_name"
fi
opt_base="longopt_$opt_name"
else
: $(( positional+=1 ))
opt_base="positional_$positional"
is_positional=1
fi
if test -n "$opt_base" ; then
eval "local occurred_$opt_base=1"
eval "local act_1=\"\${act_1_$opt_base:-}\""
eval "local varname=\"\${varname_$opt_base:-}\""
local need_val=
local func=
case "$act_1" in
(store_const)
eval "local const=\"\${act_2_${opt_base}:-1}\""
eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
printf 'local %s=%s\n' "$varname" "$const"
;;
(store)
eval "varname=\"\${act_2_${opt_base}:-$varname}\""
need_val=1
;;
(run)
eval "func=\"\${act_2_${opt_base}}\""
eval "varname=\"\${act_3_${opt_base}:-$varname}\""
printf 'local %s="$(%s)"\n' "$varname" "$func"
;;
(modify)
eval "func=\"\${act_2_${opt_base}}\""
eval "varname=\"\${act_3_${opt_base}:-$varname}\""
need_val=1
;;
("")
getopts_error --help "Wrong argument: $argument"
;;
esac
if test -n "$need_val" ; then
local val=
if test -z "$is_positional" ; then
if test $has_equal = 1 ; then
val="$equal_arg"
else
if test $# -eq 0 ; then
getopts_error "Missing argument for $opt_name"
fi
val="$1" ; shift
fi
else
val="$argument"
fi
local escaped_val="'$(printf "%s" "$val" | sed "s/'/'\\\\''/g")'"
case "$act_1" in
(store)
printf 'local %s=%s\n' "$varname" "$escaped_val"
;;
(modify)
printf 'local %s="$(%s %s)"\n' "$varname" "$func" "$escaped_val"
;;
esac
fi
fi
done
# Print default values when no values were provided
local opt_base=
for opt_base in $opt_bases ; do
eval "local occurred=\"\${occurred_$opt_base:-}\""
if test -n "$occurred" ; then
continue
fi
eval "local act_1=\"\$act_1_$opt_base\""
eval "local varname=\"\$varname_$opt_base\""
case "$act_1" in
(store)
eval "local varname=\"\${act_2_${opt_base}:-$varname}\""
eval "local default=\"\${act_3_${opt_base}:-}\""
printf 'local %s=%s\n' "$varname" "$default"
;;
(store_const|run|modify)
eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
eval "local default=\"\${act_4_${opt_base}:-}\""
printf 'local %s=%s\n' "$varname" "$default"
;;
esac
done
}
get_pvs_comment() {
local tgt="$1" ; shift
cat > "$tgt/pvs-comment" << EOF
// This is an open source non-commercial project. Dear PVS-Studio, please check
// it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
EOF
}
install_pvs() {(
local tgt="$1" ; shift
local pvs_url="$1" ; shift
cd "$tgt"
if test -d pvs-studio ; then
log_info 'install_pvs: "pvs-studio" directory already exists, skipping install'
return 0
fi
mkdir pvs-studio
cd pvs-studio
curl -L -o pvs-studio.tar.gz "$pvs_url"
tar xzf pvs-studio.tar.gz
rm pvs-studio.tar.gz
local pvsdir="$(find . -maxdepth 1 -mindepth 1)"
find "$pvsdir" -maxdepth 1 -mindepth 1 -exec mv '{}' . \;
rmdir "$pvsdir"
)}
create_compile_commands() {(
local tgt="$1" ; shift
local deps="$1" ; shift
local environment_cc="$1" ; shift
if test -z "$environment_cc" ; then
export CC=clang
fi
export CFLAGS=' -O0 '
if test -z "$deps" ; then
mkdir -p "$tgt/build"
(
cd "$tgt/build"
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="$PWD/root"
make -j"$(get_jobs_num)"
)
else
(
cd "$tgt"
make -j"$(get_jobs_num)" CMAKE_EXTRA_FLAGS=" -DCMAKE_INSTALL_PREFIX=$PWD/root -DCMAKE_BUILD_TYPE=Debug "
)
fi
find "$tgt/build/src/nvim/auto" -name '*.test-include.c' -delete
)}
# Warning: realdir below only cares about directories unlike realpath.
#
# realpath is not available in Ubuntu trusty yet.
realdir() {(
local dir="$1"
local add=""
while ! cd "$dir" 2>/dev/null ; do
add="${dir##*/}/$add"
local new_dir="${dir%/*}"
if test "$new_dir" = "$dir" ; then
return 1
fi
dir="$new_dir"
done
printf '%s\n' "$PWD/$add"
)}
patch_sources() {(
local tgt="$1" ; shift
local only_build="${1}" ; shift
get_pvs_comment "$tgt"
local sh_script='
pvs_comment="$(cat pvs-comment ; echo -n EOS)"
filehead="$(head -c $(( ${#pvs_comment} - 3 )) "$1" ; echo -n EOS)"
if test "x$filehead" != "x$pvs_comment" ; then
cat pvs-comment "$1" > "$1.tmp"
mv "$1.tmp" "$1"
fi
'
cd "$tgt"
if test "$only_build" != "--only-build" ; then
find \
src/nvim test/functional/fixtures test/unit/fixtures \
\( -name '*.c' -a '!' -path '*xdiff*' \) \
-exec /bin/sh -c "$sh_script" - '{}' \;
fi
find \
build/src/nvim/auto build/config \
-name '*.c' -not -name '*.test-include.c' \
-exec /bin/sh -c "$sh_script" - '{}' \;
rm pvs-comment
)}
run_analysis() {(
local tgt="$1" ; shift
cd "$tgt"
if [ ! -r PVS-Studio.lic ]; then
pvs-studio-analyzer credentials -o PVS-Studio.lic 'PVS-Studio Free' 'FREE-FREE-FREE-FREE'
fi
# pvs-studio-analyzer exits with a non-zero exit code when there are detected
# errors, so ignore its return
pvs-studio-analyzer \
analyze \
--lic-file PVS-Studio.lic \
--threads "$(get_jobs_num)" \
--exclude-path src/cjson \
--exclude-path src/xdiff \
--output-file PVS-studio.log \
--file build/compile_commands.json \
--sourcetree-root . || true
rm -rf PVS-studio.{xml,err,tsk,html.d}
local plog_args="PVS-studio.log --srcRoot . --excludedCodes V002,V011,V1028,V1042,V1051,V1074"
plog-converter $plog_args --renderTypes xml --output PVS-studio.xml
plog-converter $plog_args --renderTypes errorfile --output PVS-studio.err
plog-converter $plog_args --renderTypes tasklist --output PVS-studio.tsk
plog-converter $plog_args --renderTypes fullhtml --output PVS-studio.html.d
)}
detect_url() {
local url="${1:-detect}"
if test "$url" = detect ; then
curl --silent -L 'https://pvs-studio.com/en/pvs-studio/download-all/' \
| grep -o 'https\{0,1\}://[^"<>]\{1,\}/pvs-studio[^/"<>]*-x86_64\.tgz' \
|| echo FAILED
else
printf '%s' "$url"
fi
}
do_check() {
local tgt="$1" ; shift
local branch="$1" ; shift
local pvs_url="$1" ; shift
local deps="$1" ; shift
local environment_cc="$1" ; shift
if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
pvs_url="$(detect_url detect)"
if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
echo "failed to auto-detect PVS URL"
exit 1
fi
echo "Auto-detected PVS URL: ${pvs_url}"
fi
git clone --branch="$branch" . "$tgt"
install_pvs "$tgt" "$pvs_url"
do_recheck "$tgt" "$deps" "$environment_cc" ""
}
do_recheck() {
local tgt="$1" ; shift
local deps="$1" ; shift
local environment_cc="$1" ; shift
local update="$1" ; shift
if test -n "$update" ; then
(
cd "$tgt"
local branch="$(git rev-parse --abbrev-ref HEAD)"
git checkout --detach
git fetch -f origin "${branch}:${branch}"
git checkout -f "$branch"
)
fi
create_compile_commands "$tgt" "$deps" "$environment_cc"
do_analysis "$tgt"
}
do_analysis() {
local tgt="$1" ; shift
if test -d "$tgt/pvs-studio" ; then
local saved_pwd="$PWD"
cd "$tgt/pvs-studio"
export PATH="$PWD/bin${PATH+:}${PATH}"
cd "$saved_pwd"
fi
run_analysis "$tgt"
}
main() {
eval "$(
getopts_long \
help store_const \
pvs 'modify detect_url pvs_url' \
patch store_const \
only-build 'store_const --only-build' \
recheck store_const \
only-analyse store_const \
pvs-install store_const \
deps store_const \
environment-cc store_const \
update store_const \
-- \
'modify realdir tgt "$PWD/../neovim-pvs"' \
'store branch master' \
-- "$@"
)"
if test -n "$help" ; then
help
return 0
fi
if test -n "$patch" ; then
patch_sources "$tgt" "$only_build"
elif test -n "$pvs_install" ; then
install_pvs "$tgt" "$pvs_url"
elif test -n "$recheck" ; then
do_recheck "$tgt" "$deps" "$environment_cc" "$update"
elif test -n "$only_analyse" ; then
do_analysis "$tgt"
else
do_check "$tgt" "$branch" "$pvs_url" "$deps" "$environment_cc"
fi
}
main "$@"