iwyu.sh 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. #!/usr/bin/env bash
  2. set -euo pipefail
  3. ROOT_DIR=$(cd "$(dirname "$0")/$(dirname "$(test -L "$0" && readlink "$0" || echo "/")")"; pwd)
  4. WORKSPACE_DIR="${ROOT_DIR}/../.."
  5. if [ "${OSTYPE-}" = msys ] && [ -z "${MINGW_DIR+x}" ]; then
  6. # On Windows MINGW_DIR (analogous to /usr) might not always be defined when we need it for some tools
  7. if [ "${HOSTTYPE-}" = x86_64 ]; then
  8. MINGW_DIR=/mingw64
  9. elif [ "${HOSTTYPE-}" = i686 ]; then
  10. MINGW_DIR=/mingw32
  11. fi
  12. fi
  13. invoke_cc() {
  14. local env_vars=() args=() env_parsed=0 result=0
  15. if [ "${OSTYPE}" = msys ]; then
  16. # On Windows we don't want automatic path conversion, since it can break non-path arguments
  17. env_vars+=("MSYS2_ARG_CONV_EXCL=*")
  18. fi
  19. local arg; for arg in "$@"; do
  20. if [ 0 -ne "${env_parsed}" ]; then
  21. # Nothing to do if we've already finished converting environment variables.
  22. args+=("${arg}")
  23. elif [ "${arg}" == "--" ]; then
  24. # Reached '--'? Then we're finished parsing environment variables.
  25. env_parsed=1
  26. else
  27. if [ "${OSTYPE}" = msys ] && [ ! "${arg}" = "${arg#PATH=}" ]; then
  28. # We need to split Windows-style paths and convert them to UNIX-style one-by-one.
  29. local key="${arg%%=*}" # name of environment variable
  30. local value="${arg#*=}" # value of environment variable
  31. value="${value//'/'\\''}" # Escape single quotes
  32. value="'${value//;/\' \'}'" # Replace semicolons by spaces for splitting
  33. local paths; declare -a paths="(${value})" # Split into array
  34. value=""
  35. local path; for path in "${paths[@]}"; do
  36. if [ "${OSTYPE}" = msys ]; then
  37. path="${path//\\//}" # Convert 'C:\...' into 'C:/...'
  38. fi
  39. case "${path}" in
  40. [a-zA-Z]:|[a-zA-Z]:/*)
  41. # Convert 'C:/...' into '/c/...'
  42. local drive; drive="${path%%:*}"
  43. path="/${drive,,}${path#*:}"
  44. ;;
  45. *) true;;
  46. esac
  47. if [ -n "${value}" ]; then
  48. value="${value}:"
  49. fi
  50. value="${value}${path}" # Replace with modified PATH
  51. done
  52. arg="${key}=${value}" # Replace the environment variable with the modified version
  53. fi
  54. env_vars+=("${arg}")
  55. fi
  56. done
  57. local cc; cc="${args[0]}"
  58. case "${cc##*/}" in
  59. clang*)
  60. # Call iwyu with the modified arguments and environment variables (env -i starts with a blank slate)
  61. local output
  62. # shellcheck disable=SC2016
  63. output="$(PATH="${PATH}:/usr/bin" env -i "${env_vars[@]}" "${SHELL-/bin/bash}" -c 'iwyu -isystem "$("$1" -print-resource-dir "${@:2}")/include" "${@:2}"' exec "${args[@]}" 3>&1 1>&2 2>&3- || true)." || result=$?
  64. output="${output%.}"
  65. if [ 0 -eq "${result}" ]; then
  66. printf "%s" "${output}"
  67. else
  68. printf "%s" "${output}" 1>&2
  69. fi
  70. ;;
  71. esac
  72. return "${result}"
  73. }
  74. _fix_include() {
  75. "$(command -v python2 || echo python)" "$(command -v fix_include)" "$@"
  76. }
  77. process() {
  78. # TODO(mehrdadn): Install later versions of iwyu that avoid suggesting <bits/...> headers
  79. case "${OSTYPE}" in
  80. linux*)
  81. sudo apt-get install -qq -o=Dpkg::Use-Pty=0 clang iwyu
  82. ;;
  83. esac
  84. local target
  85. target="$1"
  86. CC=clang bazel build -k --config=iwyu "${target}"
  87. CC=clang bazel aquery -k --config=iwyu --output=textproto "${target}" |
  88. "$(command -v python3 || echo python)" "${ROOT_DIR}"/bazel.py textproto2json |
  89. jq -s -r '{
  90. "artifacts": map(select(.[0] == "artifacts") | (.[1] | map({(.[0]): .[1]}) | add) | {(.id): .exec_path}) | add,
  91. "outputs": map(select(.[0] == "actions") | (.[1] | map({(.[0]): .[1]}) | add | select(.mnemonic == "iwyu_action") | .output_ids))
  92. } | (. as $parent | .outputs | map($parent.artifacts[.])) | .[]' |
  93. sed "s|^|$(bazel info | sed -n "s/execution_root: //p")/|" |
  94. xargs -r -I {} -- sed -e "/^clang: /d" -e "s|[^ ]*_virtual_includes/[^/]*/||g" {} |
  95. (
  96. # Checkout & modify a clean copy of the repo to affecting the current one
  97. local data new_worktree args=(--nocomments)
  98. data="$(cat)"
  99. new_worktree="$(TMPDIR="${WORKSPACE_DIR}/.." mktemp -d)"
  100. git worktree add -q "${new_worktree}"
  101. pushd "${new_worktree}"
  102. echo "${data}" | _fix_include "${args[@]}" || true # HACK: For files are only accessible from the workspace root
  103. echo "${data}" | { cd src && _fix_include "${args[@]}"; } || true # For files accessible from src/
  104. git diff
  105. popd # this is required so we can remove the worktree when we're done
  106. git worktree remove --force "${new_worktree}"
  107. )
  108. }
  109. postbuild() {
  110. # Parsing might fail due to various things like aspects (e.g. BazelCcProtoAspect), but we don't want to check generated files anyway
  111. local data="" # initialize in case next line fails
  112. data="$(exec "$1" --decode=blaze.CppCompileInfo "$2" < "${3#*=}" 2>&-)" || true
  113. if [ -n "${data}" ]; then
  114. # Convert output to JSON-like format so we can parse it
  115. data="$(exec sed -e "/^[a-zA-Z]/d" -e "s/^\(\s*\)\([0-9]\+\):\s*\(.*\)/\1[\2, \3],/g" -e "s/^\(\s*\)\([0-9]\+\)\s*{/\1[\2, /g" -e "s/^\(\s*\)}/\1],/g" <<< "${data}")"
  116. # Turn everything into a single line by replacing newlines with a special character that should never appear in the actual stream
  117. data="$(exec tr "\n" "\a" <<< "${data}")"
  118. # Remove some fields that we don't want, and put back newlines we removed
  119. data="$(exec sed -e "s/\(0x[0-9a-fA-F]*\)]\(,\a\)/\"\1\"]\2/g" -e "s/,\(\a\s*\(]\|\$\)\)/\1/g" -e "s/\a/\n/g" <<< "${data}")"
  120. # Parse the resulting JSON and select the actual fields we're interested in.
  121. # We put the environment variables first, separating them from the command-line arguments via '--'.
  122. # shellcheck disable=SC1003
  123. data="$(PATH="${PATH}:${MINGW_DIR-/usr}/bin" && exec jq -r '(
  124. []
  125. + [.[1:][] | select (.[0] == 6) | "\(.[1][1])=\(.[2][1])" | gsub("'\''"; "'\''\\'\'''\''") | "'\''\(.)'\''"]
  126. + ["--"]
  127. + [.[1:][] | select (.[0] == 1) | .[1] | gsub("'\''"; "'\''\\'\'''\''") | "'\''\(.)'\''"]
  128. + [.[1:][] | select (.[0] == 2 and (.[1] | type) == "string") | .[1] ]
  129. ) | .[]' <<< "${data}")"
  130. # On Windows, jq can insert carriage returns; remove them
  131. data="$(exec tr -d "\r" <<< "${data}")"
  132. # Wrap everything into a single line for passing as argument array
  133. data="$(exec tr "\n" " " <<< "${data}")"
  134. eval invoke_cc "${data}"
  135. fi
  136. }
  137. "$@"