pvscheck.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489
  1. #!/bin/sh
  2. # Assume that "local" is available.
  3. # shellcheck disable=SC2039
  4. set -e
  5. # Note: -u causes problems with posh, it barks at “undefined” $@ when no
  6. # arguments provided.
  7. test -z "$POSH_VERSION" && set -u
  8. log_info() {
  9. >&2 printf "pvscheck.sh: %s\n" "$@"
  10. }
  11. get_jobs_num() {
  12. if [ -n "${TRAVIS:-}" ] ; then
  13. # HACK: /proc/cpuinfo on Travis CI is misleading, so hardcode 1.
  14. echo 1
  15. else
  16. echo $(( $(grep -c "^processor" /proc/cpuinfo) + 1 ))
  17. fi
  18. }
  19. help() {
  20. echo 'Usage:'
  21. echo ' pvscheck.sh [--pvs URL] [--deps] [--environment-cc]'
  22. echo ' [target-directory [branch]]'
  23. echo ' pvscheck.sh [--pvs URL] [--recheck] [--environment-cc] [--update]'
  24. echo ' [target-directory]'
  25. echo ' pvscheck.sh [--pvs URL] --only-analyse [target-directory]'
  26. echo ' pvscheck.sh [--pvs URL] --pvs-install {target-directory}'
  27. echo ' pvscheck.sh --patch [--only-build]'
  28. echo
  29. echo ' --pvs: Fetch pvs-studio from URL.'
  30. echo
  31. echo ' --pvs detect: Auto-detect latest version (by scraping viva64.com).'
  32. echo
  33. echo ' --deps: (for regular run) Use top-level Makefile and build deps.'
  34. echo ' Without this it assumes all dependencies are already'
  35. echo ' installed.'
  36. echo
  37. echo ' --environment-cc: (for regular run and --recheck) Do not export'
  38. echo ' CC=clang. Build is still run with CFLAGS=-O0.'
  39. echo
  40. echo ' --only-build: (for --patch) Only patch files in ./build directory.'
  41. echo
  42. echo ' --pvs-install: Only install PVS-studio to the specified location.'
  43. echo
  44. echo ' --patch: patch sources in the current directory.'
  45. echo ' Does not patch already patched files.'
  46. echo ' Does not run analysis.'
  47. echo
  48. echo ' --recheck: run analysis on a prepared target directory.'
  49. echo
  50. echo ' --update: when rechecking first do a pull.'
  51. echo
  52. echo ' --only-analyse: run analysis on a prepared target directory '
  53. echo ' without building Neovim.'
  54. echo
  55. echo ' target-directory: Directory where build should occur.'
  56. echo ' Default: ../neovim-pvs'
  57. echo
  58. echo ' branch: Branch to check.'
  59. echo ' Default: master.'
  60. }
  61. getopts_error() {
  62. local msg="$1" ; shift
  63. local do_help=
  64. if test "$msg" = "--help" ; then
  65. msg="$1" ; shift
  66. do_help=1
  67. fi
  68. printf '%s\n' "$msg" >&2
  69. if test -n "$do_help" ; then
  70. printf '\n' >&2
  71. help >&2
  72. fi
  73. echo 'return 1'
  74. return 1
  75. }
  76. # Usage `eval "$(getopts_long long_defs -- positionals_defs -- "$@")"`
  77. #
  78. # long_defs: list of pairs of arguments like `longopt action`.
  79. # positionals_defs: list of arguments like `action`.
  80. #
  81. # `action` is a space-separated commands:
  82. #
  83. # store_const [const] [varname] [default]
  84. # Store constant [const] (default 1) (note: eval’ed) if argument is present
  85. # (long options only). Assumes long option accepts no arguments.
  86. # store [varname] [default]
  87. # Store value. Assumes long option needs an argument.
  88. # run {func} [varname] [default]
  89. # Run function {func} and store its output to the [varname]. Assumes no
  90. # arguments accepted (long options only).
  91. # modify {func} [varname] [default]
  92. # Like run, but assumes a single argument, passed to function {func} as $1.
  93. #
  94. # All actions stores empty value if neither [varname] nor [default] are
  95. # present. [default] is evaled by top-level `eval`, so be careful. Also note
  96. # that no arguments may contain spaces, including [default] and [const].
  97. getopts_long() {
  98. local positional=
  99. local opt_bases=""
  100. while test $# -gt 0 ; do
  101. local arg="$1" ; shift
  102. local opt_base=
  103. local act=
  104. local opt_name=
  105. if test -z "$positional" ; then
  106. if test "$arg" = "--" ; then
  107. positional=0
  108. continue
  109. fi
  110. act="$1" ; shift
  111. opt_name="$(echo "$arg" | tr '-' '_')"
  112. opt_base="longopt_$opt_name"
  113. else
  114. if test "$arg" = "--" ; then
  115. break
  116. fi
  117. : $(( positional+=1 ))
  118. act="$arg"
  119. opt_name="arg_$positional"
  120. opt_base="positional_$positional"
  121. fi
  122. opt_bases="$opt_bases $opt_base"
  123. eval "local varname_$opt_base=$opt_name"
  124. local i=0
  125. for act_subarg in $act ; do
  126. eval "local act_$(( i+=1 ))_$opt_base=\"\$act_subarg\""
  127. done
  128. done
  129. # Process options
  130. local positional=0
  131. local force_positional=
  132. while test $# -gt 0 ; do
  133. local argument="$1" ; shift
  134. local opt_base=
  135. local has_equal=
  136. local equal_arg=
  137. local is_positional=
  138. if test "$argument" = "--" ; then
  139. force_positional=1
  140. continue
  141. elif test -z "$force_positional" && test "${argument#--}" != "$argument"
  142. then
  143. local opt_name="${argument#--}"
  144. local opt_name_striparg="${opt_name%%=*}"
  145. if test "$opt_name" = "$opt_name_striparg" ; then
  146. has_equal=0
  147. else
  148. has_equal=1
  149. equal_arg="${argument#*=}"
  150. opt_name="$opt_name_striparg"
  151. fi
  152. # Use trailing x to prevent stripping newlines
  153. opt_name="$(printf '%sx' "$opt_name" | tr '-' '_')"
  154. opt_name="${opt_name%x}"
  155. if test -n "$(printf '%sx' "$opt_name" | tr -d 'a-z_')" ; then
  156. getopts_error "Option contains invalid characters: $opt_name"
  157. fi
  158. opt_base="longopt_$opt_name"
  159. else
  160. : $(( positional+=1 ))
  161. opt_base="positional_$positional"
  162. is_positional=1
  163. fi
  164. if test -n "$opt_base" ; then
  165. eval "local occurred_$opt_base=1"
  166. eval "local act_1=\"\${act_1_$opt_base:-}\""
  167. eval "local varname=\"\${varname_$opt_base:-}\""
  168. local need_val=
  169. local func=
  170. case "$act_1" in
  171. (store_const)
  172. eval "local const=\"\${act_2_${opt_base}:-1}\""
  173. eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
  174. printf 'local %s=%s\n' "$varname" "$const"
  175. ;;
  176. (store)
  177. eval "varname=\"\${act_2_${opt_base}:-$varname}\""
  178. need_val=1
  179. ;;
  180. (run)
  181. eval "func=\"\${act_2_${opt_base}}\""
  182. eval "varname=\"\${act_3_${opt_base}:-$varname}\""
  183. printf 'local %s="$(%s)"\n' "$varname" "$func"
  184. ;;
  185. (modify)
  186. eval "func=\"\${act_2_${opt_base}}\""
  187. eval "varname=\"\${act_3_${opt_base}:-$varname}\""
  188. need_val=1
  189. ;;
  190. ("")
  191. getopts_error --help "Wrong argument: $argument"
  192. ;;
  193. esac
  194. if test -n "$need_val" ; then
  195. local val=
  196. if test -z "$is_positional" ; then
  197. if test $has_equal = 1 ; then
  198. val="$equal_arg"
  199. else
  200. if test $# -eq 0 ; then
  201. getopts_error "Missing argument for $opt_name"
  202. fi
  203. val="$1" ; shift
  204. fi
  205. else
  206. val="$argument"
  207. fi
  208. local escaped_val="'$(printf "%s" "$val" | sed "s/'/'\\\\''/g")'"
  209. case "$act_1" in
  210. (store)
  211. printf 'local %s=%s\n' "$varname" "$escaped_val"
  212. ;;
  213. (modify)
  214. printf 'local %s="$(%s %s)"\n' "$varname" "$func" "$escaped_val"
  215. ;;
  216. esac
  217. fi
  218. fi
  219. done
  220. # Print default values when no values were provided
  221. local opt_base=
  222. for opt_base in $opt_bases ; do
  223. eval "local occurred=\"\${occurred_$opt_base:-}\""
  224. if test -n "$occurred" ; then
  225. continue
  226. fi
  227. eval "local act_1=\"\$act_1_$opt_base\""
  228. eval "local varname=\"\$varname_$opt_base\""
  229. case "$act_1" in
  230. (store)
  231. eval "local varname=\"\${act_2_${opt_base}:-$varname}\""
  232. eval "local default=\"\${act_3_${opt_base}:-}\""
  233. printf 'local %s=%s\n' "$varname" "$default"
  234. ;;
  235. (store_const|run|modify)
  236. eval "local varname=\"\${act_3_${opt_base}:-$varname}\""
  237. eval "local default=\"\${act_4_${opt_base}:-}\""
  238. printf 'local %s=%s\n' "$varname" "$default"
  239. ;;
  240. esac
  241. done
  242. }
  243. get_pvs_comment() {
  244. local tgt="$1" ; shift
  245. cat > "$tgt/pvs-comment" << EOF
  246. // This is an open source non-commercial project. Dear PVS-Studio, please check
  247. // it. PVS-Studio Static Code Analyzer for C, C++ and C#: http://www.viva64.com
  248. EOF
  249. }
  250. install_pvs() {(
  251. local tgt="$1" ; shift
  252. local pvs_url="$1" ; shift
  253. cd "$tgt"
  254. if test -d pvs-studio ; then
  255. log_info 'install_pvs: "pvs-studio" directory already exists, skipping install'
  256. return 0
  257. fi
  258. mkdir pvs-studio
  259. cd pvs-studio
  260. curl -L -o pvs-studio.tar.gz "$pvs_url"
  261. tar xzf pvs-studio.tar.gz
  262. rm pvs-studio.tar.gz
  263. local pvsdir="$(find . -maxdepth 1 -mindepth 1)"
  264. find "$pvsdir" -maxdepth 1 -mindepth 1 -exec mv '{}' . \;
  265. rmdir "$pvsdir"
  266. )}
  267. create_compile_commands() {(
  268. local tgt="$1" ; shift
  269. local deps="$1" ; shift
  270. local environment_cc="$1" ; shift
  271. if test -z "$environment_cc" ; then
  272. export CC=clang
  273. fi
  274. export CFLAGS=' -O0 '
  275. if test -z "$deps" ; then
  276. mkdir -p "$tgt/build"
  277. (
  278. cd "$tgt/build"
  279. cmake .. -DCMAKE_BUILD_TYPE=Debug -DCMAKE_INSTALL_PREFIX="$PWD/root"
  280. make -j"$(get_jobs_num)"
  281. )
  282. else
  283. (
  284. cd "$tgt"
  285. make -j"$(get_jobs_num)" CMAKE_EXTRA_FLAGS=" -DCMAKE_INSTALL_PREFIX=$PWD/root -DCMAKE_BUILD_TYPE=Debug "
  286. )
  287. fi
  288. find "$tgt/build/src/nvim/auto" -name '*.test-include.c' -delete
  289. )}
  290. # Warning: realdir below only cares about directories unlike realpath.
  291. #
  292. # realpath is not available in Ubuntu trusty yet.
  293. realdir() {(
  294. local dir="$1"
  295. local add=""
  296. while ! cd "$dir" 2>/dev/null ; do
  297. add="${dir##*/}/$add"
  298. local new_dir="${dir%/*}"
  299. if test "$new_dir" = "$dir" ; then
  300. return 1
  301. fi
  302. dir="$new_dir"
  303. done
  304. printf '%s\n' "$PWD/$add"
  305. )}
  306. patch_sources() {(
  307. local tgt="$1" ; shift
  308. local only_bulid="${1}" ; shift
  309. get_pvs_comment "$tgt"
  310. local sh_script='
  311. pvs_comment="$(cat pvs-comment ; echo -n EOS)"
  312. filehead="$(head -c $(( ${#pvs_comment} - 3 )) "$1" ; echo -n EOS)"
  313. if test "x$filehead" != "x$pvs_comment" ; then
  314. cat pvs-comment "$1" > "$1.tmp"
  315. mv "$1.tmp" "$1"
  316. fi
  317. '
  318. cd "$tgt"
  319. if test "$only_build" != "--only-build" ; then
  320. find \
  321. src/nvim test/functional/fixtures test/unit/fixtures \
  322. -name '*.c' \
  323. -exec /bin/sh -c "$sh_script" - '{}' \;
  324. fi
  325. find \
  326. build/src/nvim/auto build/config \
  327. -name '*.c' -not -name '*.test-include.c' \
  328. -exec /bin/sh -c "$sh_script" - '{}' \;
  329. rm pvs-comment
  330. )}
  331. run_analysis() {(
  332. local tgt="$1" ; shift
  333. cd "$tgt"
  334. # pvs-studio-analyzer exits with a non-zero exit code when there are detected
  335. # errors, so ignore its return
  336. pvs-studio-analyzer \
  337. analyze \
  338. --threads "$(get_jobs_num)" \
  339. --output-file PVS-studio.log \
  340. --file build/compile_commands.json \
  341. --sourcetree-root . || true
  342. rm -rf PVS-studio.{xml,err,tsk,html.d}
  343. local plog_args="PVS-studio.log --srcRoot . --excludedCodes V011"
  344. plog-converter $plog_args --renderTypes xml --output PVS-studio.xml
  345. plog-converter $plog_args --renderTypes errorfile --output PVS-studio.err
  346. plog-converter $plog_args --renderTypes tasklist --output PVS-studio.tsk
  347. plog-converter $plog_args --renderTypes fullhtml --output PVS-studio.html.d
  348. )}
  349. detect_url() {
  350. local url="${1:-detect}"
  351. if test "$url" = detect ; then
  352. curl --silent -L 'https://www.viva64.com/en/pvs-studio-download/' \
  353. | grep -o 'https\{0,1\}://[^"<>]\{1,\}/pvs-studio[^/"<>]*-x86_64\.tgz' \
  354. || echo FAILED
  355. else
  356. printf '%s' "$url"
  357. fi
  358. }
  359. do_check() {
  360. local tgt="$1" ; shift
  361. local branch="$1" ; shift
  362. local pvs_url="$1" ; shift
  363. local deps="$1" ; shift
  364. local environment_cc="$1" ; shift
  365. if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
  366. pvs_url="$(detect_url detect)"
  367. if test -z "$pvs_url" || test "$pvs_url" = FAILED ; then
  368. echo "failed to auto-detect PVS URL"
  369. exit 1
  370. fi
  371. echo "Auto-detected PVS URL: ${pvs_url}"
  372. fi
  373. git clone --branch="$branch" . "$tgt"
  374. install_pvs "$tgt" "$pvs_url"
  375. do_recheck "$tgt" "$deps" "$environment_cc" ""
  376. }
  377. do_recheck() {
  378. local tgt="$1" ; shift
  379. local deps="$1" ; shift
  380. local environment_cc="$1" ; shift
  381. local update="$1" ; shift
  382. if test -n "$update" ; then
  383. (
  384. cd "$tgt"
  385. local branch="$(git rev-parse --abbrev-ref HEAD)"
  386. git checkout --detach
  387. git fetch -f origin "${branch}:${branch}"
  388. git checkout -f "$branch"
  389. )
  390. fi
  391. create_compile_commands "$tgt" "$deps" "$environment_cc"
  392. do_analysis "$tgt"
  393. }
  394. do_analysis() {
  395. local tgt="$1" ; shift
  396. if test -d "$tgt/pvs-studio" ; then
  397. local saved_pwd="$PWD"
  398. cd "$tgt/pvs-studio"
  399. export PATH="$PWD/bin${PATH+:}${PATH}"
  400. cd "$saved_pwd"
  401. fi
  402. run_analysis "$tgt"
  403. }
  404. main() {
  405. eval "$(
  406. getopts_long \
  407. help store_const \
  408. pvs 'modify detect_url pvs_url' \
  409. patch store_const \
  410. only-build 'store_const --only-build' \
  411. recheck store_const \
  412. only-analyse store_const \
  413. pvs-install store_const \
  414. deps store_const \
  415. environment-cc store_const \
  416. update store_const \
  417. -- \
  418. 'modify realdir tgt "$PWD/../neovim-pvs"' \
  419. 'store branch master' \
  420. -- "$@"
  421. )"
  422. if test -n "$help" ; then
  423. help
  424. return 0
  425. fi
  426. if test -n "$patch" ; then
  427. patch_sources "$tgt" "$only_build"
  428. elif test -n "$pvs_install" ; then
  429. install_pvs "$tgt" "$pvs_url"
  430. elif test -n "$recheck" ; then
  431. do_recheck "$tgt" "$deps" "$environment_cc" "$update"
  432. elif test -n "$only_analyse" ; then
  433. do_analysis "$tgt"
  434. else
  435. do_check "$tgt" "$branch" "$pvs_url" "$deps" "$environment_cc"
  436. fi
  437. }
  438. main "$@"