check-uapi.sh 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465
  1. #!/bin/bash
  2. # SPDX-License-Identifier: GPL-2.0-only
  3. # Script to check commits for UAPI backwards compatibility
  4. set -o errexit
  5. set -o pipefail
  6. print_usage() {
  7. name=$(basename "$0")
  8. cat << EOF
  9. $name - check for UAPI header stability across Git commits
  10. By default, the script will check to make sure the latest commit (or current
  11. dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
  12. check against additional commit ranges with the -b and -p options.
  13. The script will not check UAPI headers for architectures other than the one
  14. defined in ARCH.
  15. Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-q] [-v]
  16. Options:
  17. -b BASE_REF Base git reference to use for comparison. If unspecified or empty,
  18. will use any dirty changes in tree to UAPI files. If there are no
  19. dirty changes, HEAD will be used.
  20. -p PAST_REF Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
  21. will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
  22. that exist on PAST_REF will be checked for compatibility.
  23. -j JOBS Number of checks to run in parallel (default: number of CPU cores).
  24. -l ERROR_LOG Write error log to file (default: no error log is generated).
  25. -q Quiet operation (suppress stdout, still print stderr).
  26. -v Verbose operation (print more information about each header being checked).
  27. Environmental args:
  28. ABIDIFF Custom path to abidiff binary
  29. CC C compiler (default is "gcc")
  30. ARCH Target architecture of C compiler (default is host arch)
  31. Exit codes:
  32. $SUCCESS) Success
  33. $FAIL_ABI) ABI difference detected
  34. $FAIL_PREREQ) Prerequisite not met
  35. $FAIL_COMPILE) Compilation error
  36. EOF
  37. }
  38. readonly SUCCESS=0
  39. readonly FAIL_ABI=1
  40. readonly FAIL_PREREQ=2
  41. readonly FAIL_COMPILE=3
  42. # Print to stderr
  43. eprintf() {
  44. # shellcheck disable=SC2059
  45. printf "$@" >&2
  46. }
  47. # Check if git tree is dirty
  48. tree_is_dirty() {
  49. if git diff --quiet; then
  50. return 1
  51. else
  52. return 0
  53. fi
  54. }
  55. # Get list of files installed in $ref
  56. get_file_list() {
  57. local -r ref="$1"
  58. local -r tree="$(get_header_tree "$ref")"
  59. # Print all installed headers, filtering out ones that can't be compiled
  60. find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
  61. }
  62. # Add to the list of incompatible headers
  63. add_to_incompat_list() {
  64. local -r ref="$1"
  65. # Start with the usr/include/Makefile to get a list of the headers
  66. # that don't compile using this method.
  67. if [ ! -f usr/include/Makefile ]; then
  68. eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
  69. eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
  70. exit "$FAIL_PREREQ"
  71. fi
  72. {
  73. # shellcheck disable=SC2016
  74. printf 'all: ; @echo $(no-header-test)\n'
  75. cat usr/include/Makefile
  76. } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
  77. | grep -v "asm-generic" >> "$INCOMPAT_LIST"
  78. # The makefile also skips all asm-generic files, but prints "asm-generic/%"
  79. # which won't work for our grep match. Instead, print something grep will match.
  80. printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
  81. sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
  82. }
  83. # Compile the simple test app
  84. do_compile() {
  85. local -r inc_dir="$1"
  86. local -r header="$2"
  87. local -r out="$3"
  88. printf "int main(void) { return 0; }\n" | \
  89. "$CC" -c \
  90. -o "$out" \
  91. -x c \
  92. -O0 \
  93. -std=c90 \
  94. -fno-eliminate-unused-debug-types \
  95. -g \
  96. "-I${inc_dir}" \
  97. -include "$header" \
  98. -
  99. }
  100. # Run make headers_install
  101. run_make_headers_install() {
  102. local -r install_dir="$1"
  103. make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
  104. headers_install > /dev/null
  105. }
  106. # Install headers for both git refs
  107. install_headers() {
  108. local -r base_ref="$1"
  109. local -r past_ref="$2"
  110. for ref in "$base_ref" "$past_ref"; do
  111. printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
  112. if [ -n "$ref" ]; then
  113. git archive --format=tar --prefix="${ref}-archive/" "$ref" \
  114. | (cd "$TMP_DIR" && tar xf -)
  115. (
  116. cd "${TMP_DIR}/${ref}-archive"
  117. run_make_headers_install "${TMP_DIR}/${ref}/usr"
  118. add_to_incompat_list "$ref" "$INCOMPAT_LIST"
  119. )
  120. else
  121. run_make_headers_install "${TMP_DIR}/${ref}/usr"
  122. add_to_incompat_list "$ref" "$INCOMPAT_LIST"
  123. fi
  124. printf "OK\n"
  125. done
  126. }
  127. # Print the path to the headers_install tree for a given ref
  128. get_header_tree() {
  129. local -r ref="$1"
  130. printf "%s" "${TMP_DIR}/${ref}/usr"
  131. }
  132. # Check file list for UAPI compatibility
  133. check_uapi_files() {
  134. local -r base_ref="$1"
  135. local -r past_ref="$2"
  136. local passed=0;
  137. local failed=0;
  138. local -a threads=()
  139. set -o errexit
  140. printf "Checking changes to UAPI headers between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
  141. # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
  142. # there's no way they're broken and no way to compare anyway)
  143. while read -r file; do
  144. if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
  145. if wait "${threads[0]}"; then
  146. passed=$((passed + 1))
  147. else
  148. failed=$((failed + 1))
  149. fi
  150. threads=("${threads[@]:1}")
  151. fi
  152. check_individual_file "$base_ref" "$past_ref" "$file" &
  153. threads+=("$!")
  154. done < <(get_file_list "$past_ref")
  155. for t in "${threads[@]}"; do
  156. if wait "$t"; then
  157. passed=$((passed + 1))
  158. else
  159. failed=$((failed + 1))
  160. fi
  161. done
  162. total="$((passed + failed))"
  163. if [ "$failed" -gt 0 ]; then
  164. eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
  165. "$failed" "$total" "$ARCH"
  166. else
  167. printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" "$total" "$ARCH"
  168. fi
  169. return "$failed"
  170. }
  171. # Check an individual file for UAPI compatibility
  172. check_individual_file() {
  173. local -r base_ref="$1"
  174. local -r past_ref="$2"
  175. local -r file="$3"
  176. local -r base_header="$(get_header_tree "$base_ref")/${file}"
  177. local -r past_header="$(get_header_tree "$past_ref")/${file}"
  178. if [ ! -f "$base_header" ]; then
  179. printf "!!! UAPI header %s was incorrectly removed between %s and %s !!!\n" \
  180. "$file" "$past_ref" "${base_ref:-dirty tree}" \
  181. | tee "${base_header}.error" >&2
  182. return 1
  183. fi
  184. compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref"
  185. }
  186. # Perform the A/B compilation and compare output ABI
  187. compare_abi() {
  188. local -r file="$1"
  189. local -r base_header="$2"
  190. local -r past_header="$3"
  191. local -r base_ref="$4"
  192. local -r past_ref="$5"
  193. local -r log="${TMP_DIR}/log/${file}.log"
  194. mkdir -p "$(dirname "$log")"
  195. if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
  196. eprintf "error - couldn't compile version of UAPI header %s at %s\n" "$file" "$base_ref"
  197. cat "$log" >&2
  198. exit "$FAIL_COMPILE"
  199. fi
  200. if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
  201. eprintf "error - couldn't compile version of UAPI header %s at %s\n" "$file" "$past_ref"
  202. cat "$log" >&2
  203. exit "$FAIL_COMPILE"
  204. fi
  205. local ret=0
  206. "$ABIDIFF" --non-reachable-types "${past_header}.bin" "${base_header}.bin" \
  207. > "$log" || ret="$?"
  208. if [ "$ret" -eq 0 ]; then
  209. if [ "$VERBOSE" = "true" ]; then
  210. printf "No ABI differences detected in %s from %s -> %s\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
  211. fi
  212. else
  213. # Bits in abidiff's return code can be used to determine the type of error
  214. if [ $((ret & 0x1)) -gt 0 ]; then
  215. eprintf "error - abidiff did not run properly\n"
  216. exit 1
  217. fi
  218. # If the only changes were additions (not modifications to existing APIs), then
  219. # there's no problem. Ignore these diffs.
  220. if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
  221. grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
  222. return 0
  223. fi
  224. {
  225. printf "!!! ABI differences detected in %s from %s -> %s !!!\n\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
  226. sed -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/ /g' "$log"
  227. if ! cmp "$past_header" "$base_header" > /dev/null 2>&1; then
  228. printf "\nHeader file diff (after headers_install):\n"
  229. diff -Naur "$past_header" "$base_header" \
  230. | sed -e "s|${past_header}|${past_ref}/${file}|g" \
  231. -e "s|${base_header}|${base_ref:-dirty}/${file}|g"
  232. printf "\n"
  233. else
  234. printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
  235. printf "It's possible a change to one of the headers it includes caused this error:\n"
  236. grep '^#include' "$base_header"
  237. printf "\n"
  238. fi
  239. } | tee "${base_header}.error" >&2
  240. return 1
  241. fi
  242. }
  243. min_version_is_satisfied() {
  244. local -r min_version="$1"
  245. local -r version_installed="$2"
  246. printf "%s\n%s\n" "$min_version" "$version_installed" | sort -Vc > /dev/null 2>&1
  247. }
  248. # Make sure we have the tools we need and the arguments make sense
  249. check_deps() {
  250. ABIDIFF="${ABIDIFF:-abidiff}"
  251. CC="${CC:-gcc}"
  252. ARCH="${ARCH:-$(uname -m)}"
  253. if [ "$ARCH" = "x86_64" ]; then
  254. ARCH="x86"
  255. fi
  256. local -r abidiff_min_version="1.7"
  257. local -r libdw_min_version_if_clang="0.171"
  258. if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
  259. eprintf "error - abidiff not found!\n"
  260. eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
  261. eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
  262. return 1
  263. fi
  264. local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
  265. if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
  266. eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
  267. eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
  268. eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
  269. return 1
  270. fi
  271. if ! command -v "$CC" > /dev/null 2>&1; then
  272. eprintf 'error - %s not found\n' "$CC"
  273. return 1
  274. fi
  275. if "$CC" --version | grep -q clang; then
  276. local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
  277. if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
  278. eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
  279. eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
  280. eprintf "See: https://sourceware.org/elfutils/\n"
  281. return 1
  282. fi
  283. fi
  284. if [ ! -d "arch/${ARCH}" ]; then
  285. eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
  286. eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
  287. return 1
  288. fi
  289. if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
  290. eprintf "error - this script requires the kernel tree to be initialized with Git\n"
  291. return 1
  292. fi
  293. if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
  294. printf 'error - invalid git reference "%s"\n' "$past_ref"
  295. return 1
  296. fi
  297. if [ -n "$base_ref" ]; then
  298. if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
  299. printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
  300. return 1
  301. fi
  302. if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
  303. printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
  304. return 1
  305. fi
  306. fi
  307. }
  308. run() {
  309. local base_ref="$1"
  310. local past_ref="$2"
  311. local abi_error_log="$3"
  312. shift 3
  313. if [ -z "$KERNEL_SRC" ]; then
  314. KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
  315. fi
  316. cd "$KERNEL_SRC"
  317. if [ -z "$base_ref" ] && ! tree_is_dirty; then
  318. base_ref=HEAD
  319. fi
  320. if [ -z "$past_ref" ]; then
  321. if [ -n "$base_ref" ]; then
  322. past_ref="${base_ref}^1"
  323. else
  324. past_ref=HEAD
  325. fi
  326. fi
  327. if ! check_deps; then
  328. exit "$FAIL_PREREQ"
  329. fi
  330. TMP_DIR=$(mktemp -d)
  331. readonly TMP_DIR
  332. trap 'rm -rf "$TMP_DIR"' EXIT
  333. readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
  334. touch "$INCOMPAT_LIST"
  335. # Run make install_headers for both refs
  336. install_headers "$base_ref" "$past_ref"
  337. # Check for any differences in the installed header trees
  338. if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
  339. printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
  340. exit "$SUCCESS"
  341. fi
  342. if ! check_uapi_files "$base_ref" "$past_ref"; then
  343. eprintf "error - UAPI header ABI check failed\n"
  344. if [ -n "$abi_error_log" ]; then
  345. {
  346. printf 'Generated by "%s %s" from git ref %s\n\n' "$0" "$*" "$(git rev-parse HEAD)"
  347. find "$TMP_DIR" -type f -name '*.error' -exec cat {} +
  348. } > "$abi_error_log"
  349. eprintf "Failure summary saved to %s\n" "$abi_error_log"
  350. fi
  351. exit "$FAIL_ABI"
  352. fi
  353. }
  354. main() {
  355. MAX_THREADS=$(nproc)
  356. VERBOSE="false"
  357. quiet="false"
  358. local base_ref=""
  359. while getopts "hb:p:mj:l:qv" opt; do
  360. case $opt in
  361. h)
  362. print_usage
  363. exit "$SUCCESS"
  364. ;;
  365. b)
  366. base_ref="$OPTARG"
  367. ;;
  368. p)
  369. past_ref="$OPTARG"
  370. ;;
  371. j)
  372. MAX_THREADS="$OPTARG"
  373. ;;
  374. l)
  375. abi_error_log="$OPTARG"
  376. ;;
  377. q)
  378. quiet="true"
  379. VERBOSE="false"
  380. ;;
  381. v)
  382. VERBOSE="true"
  383. quiet="false"
  384. ;;
  385. *)
  386. exit "$FAIL_PREREQ"
  387. esac
  388. done
  389. if [ "$quiet" = "true" ]; then
  390. exec > /dev/null 2>&1
  391. fi
  392. run "$base_ref" "$past_ref" "$abi_error_log" "$@"
  393. }
  394. main "$@"