]> git.ipfire.org Git - thirdparty/kernel/linux.git/blob - scripts/check-uapi.sh
Merge tag 'scsi-fixes' of git://git.kernel.org/pub/scm/linux/kernel/git/jejb/scsi
[thirdparty/kernel/linux.git] / scripts / check-uapi.sh
1 #!/bin/bash
2 # SPDX-License-Identifier: GPL-2.0-only
3 # Script to check commits for UAPI backwards compatibility
4
5 set -o errexit
6 set -o pipefail
7
8 print_usage() {
9 name=$(basename "$0")
10 cat << EOF
11 $name - check for UAPI header stability across Git commits
12
13 By default, the script will check to make sure the latest commit (or current
14 dirty changes) did not introduce ABI changes when compared to HEAD^1. You can
15 check against additional commit ranges with the -b and -p options.
16
17 The script will not check UAPI headers for architectures other than the one
18 defined in ARCH.
19
20 Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
21
22 Options:
23 -b BASE_REF Base git reference to use for comparison. If unspecified or empty,
24 will use any dirty changes in tree to UAPI files. If there are no
25 dirty changes, HEAD will be used.
26 -p PAST_REF Compare BASE_REF to PAST_REF (e.g. -p v6.1). If unspecified or empty,
27 will use BASE_REF^1. Must be an ancestor of BASE_REF. Only headers
28 that exist on PAST_REF will be checked for compatibility.
29 -j JOBS Number of checks to run in parallel (default: number of CPU cores).
30 -l ERROR_LOG Write error log to file (default: no error log is generated).
31 -i Ignore ambiguous changes that may or may not break UAPI compatibility.
32 -q Quiet operation.
33 -v Verbose operation (print more information about each header being checked).
34
35 Environmental args:
36 ABIDIFF Custom path to abidiff binary
37 CC C compiler (default is "gcc")
38 ARCH Target architecture for the UAPI check (default is host arch)
39
40 Exit codes:
41 $SUCCESS) Success
42 $FAIL_ABI) ABI difference detected
43 $FAIL_PREREQ) Prerequisite not met
44 EOF
45 }
46
47 readonly SUCCESS=0
48 readonly FAIL_ABI=1
49 readonly FAIL_PREREQ=2
50
51 # Print to stderr
52 eprintf() {
53 # shellcheck disable=SC2059
54 printf "$@" >&2
55 }
56
57 # Expand an array with a specific character (similar to Python string.join())
58 join() {
59 local IFS="$1"
60 shift
61 printf "%s" "$*"
62 }
63
64 # Create abidiff suppressions
65 gen_suppressions() {
66 # Common enum variant names which we don't want to worry about
67 # being shifted when new variants are added.
68 local -a enum_regex=(
69 ".*_AFTER_LAST$"
70 ".*_CNT$"
71 ".*_COUNT$"
72 ".*_END$"
73 ".*_LAST$"
74 ".*_MASK$"
75 ".*_MAX$"
76 ".*_MAX_BIT$"
77 ".*_MAX_BPF_ATTACH_TYPE$"
78 ".*_MAX_ID$"
79 ".*_MAX_SHIFT$"
80 ".*_NBITS$"
81 ".*_NETDEV_NUMHOOKS$"
82 ".*_NFT_META_IIFTYPE$"
83 ".*_NL80211_ATTR$"
84 ".*_NLDEV_NUM_OPS$"
85 ".*_NUM$"
86 ".*_NUM_ELEMS$"
87 ".*_NUM_IRQS$"
88 ".*_SIZE$"
89 ".*_TLSMAX$"
90 "^MAX_.*"
91 "^NUM_.*"
92 )
93
94 # Common padding field names which can be expanded into
95 # without worrying about users.
96 local -a padding_regex=(
97 ".*end$"
98 ".*pad$"
99 ".*pad[0-9]?$"
100 ".*pad_[0-9]?$"
101 ".*padding$"
102 ".*padding[0-9]?$"
103 ".*padding_[0-9]?$"
104 ".*res$"
105 ".*resv$"
106 ".*resv[0-9]?$"
107 ".*resv_[0-9]?$"
108 ".*reserved$"
109 ".*reserved[0-9]?$"
110 ".*reserved_[0-9]?$"
111 ".*rsvd[0-9]?$"
112 ".*unused$"
113 )
114
115 cat << EOF
116 [suppress_type]
117 type_kind = enum
118 changed_enumerators_regexp = $(join , "${enum_regex[@]}")
119 EOF
120
121 for p in "${padding_regex[@]}"; do
122 cat << EOF
123 [suppress_type]
124 type_kind = struct
125 has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
126 EOF
127 done
128
129 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
130 cat << EOF
131 [suppress_type]
132 type_kind = struct
133 has_data_member_inserted_at = end
134 has_size_change = yes
135 EOF
136 fi
137 }
138
139 # Check if git tree is dirty
140 tree_is_dirty() {
141 ! git diff --quiet
142 }
143
144 # Get list of files installed in $ref
145 get_file_list() {
146 local -r ref="$1"
147 local -r tree="$(get_header_tree "$ref")"
148
149 # Print all installed headers, filtering out ones that can't be compiled
150 find "$tree" -type f -name '*.h' -printf '%P\n' | grep -v -f "$INCOMPAT_LIST"
151 }
152
153 # Add to the list of incompatible headers
154 add_to_incompat_list() {
155 local -r ref="$1"
156
157 # Start with the usr/include/Makefile to get a list of the headers
158 # that don't compile using this method.
159 if [ ! -f usr/include/Makefile ]; then
160 eprintf "error - no usr/include/Makefile present at %s\n" "$ref"
161 eprintf "Note: usr/include/Makefile was added in the v5.3 kernel release\n"
162 exit "$FAIL_PREREQ"
163 fi
164 {
165 # shellcheck disable=SC2016
166 printf 'all: ; @echo $(no-header-test)\n'
167 cat usr/include/Makefile
168 } | SRCARCH="$ARCH" make --always-make -f - | tr " " "\n" \
169 | grep -v "asm-generic" >> "$INCOMPAT_LIST"
170
171 # The makefile also skips all asm-generic files, but prints "asm-generic/%"
172 # which won't work for our grep match. Instead, print something grep will match.
173 printf "asm-generic/.*\.h\n" >> "$INCOMPAT_LIST"
174 }
175
176 # Compile the simple test app
177 do_compile() {
178 local -r inc_dir="$1"
179 local -r header="$2"
180 local -r out="$3"
181 printf "int main(void) { return 0; }\n" | \
182 "$CC" -c \
183 -o "$out" \
184 -x c \
185 -O0 \
186 -std=c90 \
187 -fno-eliminate-unused-debug-types \
188 -g \
189 "-I${inc_dir}" \
190 -include "$header" \
191 -
192 }
193
194 # Run make headers_install
195 run_make_headers_install() {
196 local -r ref="$1"
197 local -r install_dir="$(get_header_tree "$ref")"
198 make -j "$MAX_THREADS" ARCH="$ARCH" INSTALL_HDR_PATH="$install_dir" \
199 headers_install > /dev/null
200 }
201
202 # Install headers for both git refs
203 install_headers() {
204 local -r base_ref="$1"
205 local -r past_ref="$2"
206
207 for ref in "$base_ref" "$past_ref"; do
208 printf "Installing user-facing UAPI headers from %s... " "${ref:-dirty tree}"
209 if [ -n "$ref" ]; then
210 git archive --format=tar --prefix="${ref}-archive/" "$ref" \
211 | (cd "$TMP_DIR" && tar xf -)
212 (
213 cd "${TMP_DIR}/${ref}-archive"
214 run_make_headers_install "$ref"
215 add_to_incompat_list "$ref" "$INCOMPAT_LIST"
216 )
217 else
218 run_make_headers_install "$ref"
219 add_to_incompat_list "$ref" "$INCOMPAT_LIST"
220 fi
221 printf "OK\n"
222 done
223 sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
224 sed -i -e '/^$/d' "$INCOMPAT_LIST"
225 }
226
227 # Print the path to the headers_install tree for a given ref
228 get_header_tree() {
229 local -r ref="$1"
230 printf "%s" "${TMP_DIR}/${ref}/usr"
231 }
232
233 # Check file list for UAPI compatibility
234 check_uapi_files() {
235 local -r base_ref="$1"
236 local -r past_ref="$2"
237 local -r abi_error_log="$3"
238
239 local passed=0;
240 local failed=0;
241 local -a threads=()
242 set -o errexit
243
244 printf "Checking changes to UAPI headers between %s and %s...\n" "$past_ref" "${base_ref:-dirty tree}"
245 # Loop over all UAPI headers that were installed by $past_ref (if they only exist on $base_ref,
246 # there's no way they're broken and no way to compare anyway)
247 while read -r file; do
248 if [ "${#threads[@]}" -ge "$MAX_THREADS" ]; then
249 if wait "${threads[0]}"; then
250 passed=$((passed + 1))
251 else
252 failed=$((failed + 1))
253 fi
254 threads=("${threads[@]:1}")
255 fi
256
257 check_individual_file "$base_ref" "$past_ref" "$file" &
258 threads+=("$!")
259 done < <(get_file_list "$past_ref")
260
261 for t in "${threads[@]}"; do
262 if wait "$t"; then
263 passed=$((passed + 1))
264 else
265 failed=$((failed + 1))
266 fi
267 done
268
269 if [ -n "$abi_error_log" ]; then
270 printf 'Generated by "%s %s" from git ref %s\n\n' \
271 "$0" "$*" "$(git rev-parse HEAD)" > "$abi_error_log"
272 fi
273
274 while read -r error_file; do
275 {
276 cat "$error_file"
277 printf "\n\n"
278 } | tee -a "${abi_error_log:-/dev/null}" >&2
279 done < <(find "$TMP_DIR" -type f -name '*.error' | sort)
280
281 total="$((passed + failed))"
282 if [ "$failed" -gt 0 ]; then
283 eprintf "error - %d/%d UAPI headers compatible with %s appear _not_ to be backwards compatible\n" \
284 "$failed" "$total" "$ARCH"
285 if [ -n "$abi_error_log" ]; then
286 eprintf "Failure summary saved to %s\n" "$abi_error_log"
287 fi
288 else
289 printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
290 "$total" "$ARCH"
291 fi
292
293 return "$failed"
294 }
295
296 # Check an individual file for UAPI compatibility
297 check_individual_file() {
298 local -r base_ref="$1"
299 local -r past_ref="$2"
300 local -r file="$3"
301
302 local -r base_header="$(get_header_tree "$base_ref")/${file}"
303 local -r past_header="$(get_header_tree "$past_ref")/${file}"
304
305 if [ ! -f "$base_header" ]; then
306 mkdir -p "$(dirname "$base_header")"
307 printf "==== UAPI header %s was removed between %s and %s ====" \
308 "$file" "$past_ref" "$base_ref" \
309 > "${base_header}.error"
310 return 1
311 fi
312
313 compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref"
314 }
315
316 # Perform the A/B compilation and compare output ABI
317 compare_abi() {
318 local -r file="$1"
319 local -r base_header="$2"
320 local -r past_header="$3"
321 local -r base_ref="$4"
322 local -r past_ref="$5"
323 local -r log="${TMP_DIR}/log/${file}.log"
324 local -r error_log="${TMP_DIR}/log/${file}.error"
325
326 mkdir -p "$(dirname "$log")"
327
328 if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
329 {
330 warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
331 "$file" "$base_ref")
332 printf "%s\n" "$warn_str"
333 cat "$log"
334 printf -- "=%.0s" $(seq 0 ${#warn_str})
335 } > "$error_log"
336 return 1
337 fi
338
339 if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
340 {
341 warn_str=$(printf "==== Could not compile version of UAPI header %s at %s ====\n" \
342 "$file" "$past_ref")
343 printf "%s\n" "$warn_str"
344 cat "$log"
345 printf -- "=%.0s" $(seq 0 ${#warn_str})
346 } > "$error_log"
347 return 1
348 fi
349
350 local ret=0
351 "$ABIDIFF" --non-reachable-types \
352 --suppressions "$SUPPRESSIONS" \
353 "${past_header}.bin" "${base_header}.bin" > "$log" || ret="$?"
354 if [ "$ret" -eq 0 ]; then
355 if [ "$VERBOSE" = "true" ]; then
356 printf "No ABI differences detected in %s from %s -> %s\n" \
357 "$file" "$past_ref" "$base_ref"
358 fi
359 else
360 # Bits in abidiff's return code can be used to determine the type of error
361 if [ $((ret & 0x2)) -gt 0 ]; then
362 eprintf "error - abidiff did not run properly\n"
363 exit 1
364 fi
365
366 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
367 return 0
368 fi
369
370 # If the only changes were additions (not modifications to existing APIs), then
371 # there's no problem. Ignore these diffs.
372 if grep "Unreachable types summary" "$log" | grep -q "0 removed" &&
373 grep "Unreachable types summary" "$log" | grep -q "0 changed"; then
374 return 0
375 fi
376
377 {
378 warn_str=$(printf "==== ABI differences detected in %s from %s -> %s ====" \
379 "$file" "$past_ref" "$base_ref")
380 printf "%s\n" "$warn_str"
381 sed -e '/summary:/d' -e '/changed type/d' -e '/^$/d' -e 's/^/ /g' "$log"
382 printf -- "=%.0s" $(seq 0 ${#warn_str})
383 if cmp "$past_header" "$base_header" > /dev/null 2>&1; then
384 printf "\n%s did not change between %s and %s...\n" "$file" "$past_ref" "${base_ref:-dirty tree}"
385 printf "It's possible a change to one of the headers it includes caused this error:\n"
386 grep '^#include' "$base_header"
387 printf "\n"
388 fi
389 } > "$error_log"
390
391 return 1
392 fi
393 }
394
395 # Check that a minimum software version number is satisfied
396 min_version_is_satisfied() {
397 local -r min_version="$1"
398 local -r version_installed="$2"
399
400 printf "%s\n%s\n" "$min_version" "$version_installed" \
401 | sort -Vc > /dev/null 2>&1
402 }
403
404 # Make sure we have the tools we need and the arguments make sense
405 check_deps() {
406 ABIDIFF="${ABIDIFF:-abidiff}"
407 CC="${CC:-gcc}"
408 ARCH="${ARCH:-$(uname -m)}"
409 if [ "$ARCH" = "x86_64" ]; then
410 ARCH="x86"
411 fi
412
413 local -r abidiff_min_version="2.4"
414 local -r libdw_min_version_if_clang="0.171"
415
416 if ! command -v "$ABIDIFF" > /dev/null 2>&1; then
417 eprintf "error - abidiff not found!\n"
418 eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
419 eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
420 return 1
421 fi
422
423 local -r abidiff_version="$("$ABIDIFF" --version | cut -d ' ' -f 2)"
424 if ! min_version_is_satisfied "$abidiff_min_version" "$abidiff_version"; then
425 eprintf "error - abidiff version too old: %s\n" "$abidiff_version"
426 eprintf "Please install abigail-tools version %s or greater\n" "$abidiff_min_version"
427 eprintf "See: https://sourceware.org/libabigail/manual/libabigail-overview.html\n"
428 return 1
429 fi
430
431 if ! command -v "$CC" > /dev/null 2>&1; then
432 eprintf 'error - %s not found\n' "$CC"
433 return 1
434 fi
435
436 if "$CC" --version | grep -q clang; then
437 local -r libdw_version="$(ldconfig -v 2>/dev/null | grep -v SKIPPED | grep -m 1 -o 'libdw-[0-9]\+.[0-9]\+' | cut -c 7-)"
438 if ! min_version_is_satisfied "$libdw_min_version_if_clang" "$libdw_version"; then
439 eprintf "error - libdw version too old for use with clang: %s\n" "$libdw_version"
440 eprintf "Please install libdw from elfutils version %s or greater\n" "$libdw_min_version_if_clang"
441 eprintf "See: https://sourceware.org/elfutils/\n"
442 return 1
443 fi
444 fi
445
446 if [ ! -d "arch/${ARCH}" ]; then
447 eprintf 'error - ARCH "%s" is not a subdirectory under arch/\n' "$ARCH"
448 eprintf "Please set ARCH to one of:\n%s\n" "$(find arch -maxdepth 1 -mindepth 1 -type d -printf '%f ' | fmt)"
449 return 1
450 fi
451
452 if ! git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
453 eprintf "error - this script requires the kernel tree to be initialized with Git\n"
454 return 1
455 fi
456
457 if ! git rev-parse --verify "$past_ref" > /dev/null 2>&1; then
458 printf 'error - invalid git reference "%s"\n' "$past_ref"
459 return 1
460 fi
461
462 if [ -n "$base_ref" ]; then
463 if ! git merge-base --is-ancestor "$past_ref" "$base_ref" > /dev/null 2>&1; then
464 printf 'error - "%s" is not an ancestor of base ref "%s"\n' "$past_ref" "$base_ref"
465 return 1
466 fi
467 if [ "$(git rev-parse "$base_ref")" = "$(git rev-parse "$past_ref")" ]; then
468 printf 'error - "%s" and "%s" are the same reference\n' "$past_ref" "$base_ref"
469 return 1
470 fi
471 fi
472 }
473
474 run() {
475 local base_ref="$1"
476 local past_ref="$2"
477 local abi_error_log="$3"
478 shift 3
479
480 if [ -z "$KERNEL_SRC" ]; then
481 KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
482 fi
483
484 cd "$KERNEL_SRC"
485
486 if [ -z "$base_ref" ] && ! tree_is_dirty; then
487 base_ref=HEAD
488 fi
489
490 if [ -z "$past_ref" ]; then
491 if [ -n "$base_ref" ]; then
492 past_ref="${base_ref}^1"
493 else
494 past_ref=HEAD
495 fi
496 fi
497
498 if ! check_deps; then
499 exit "$FAIL_PREREQ"
500 fi
501
502 TMP_DIR=$(mktemp -d)
503 readonly TMP_DIR
504 trap 'rm -rf "$TMP_DIR"' EXIT
505
506 readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
507 touch "$INCOMPAT_LIST"
508
509 readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
510 gen_suppressions > "$SUPPRESSIONS"
511
512 # Run make install_headers for both refs
513 install_headers "$base_ref" "$past_ref"
514
515 # Check for any differences in the installed header trees
516 if diff -r -q "$(get_header_tree "$base_ref")" "$(get_header_tree "$past_ref")" > /dev/null 2>&1; then
517 printf "No changes to UAPI headers were applied between %s and %s\n" "$past_ref" "${base_ref:-dirty tree}"
518 exit "$SUCCESS"
519 fi
520
521 if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
522 exit "$FAIL_ABI"
523 fi
524 }
525
526 main() {
527 MAX_THREADS=$(nproc)
528 VERBOSE="false"
529 IGNORE_AMBIGUOUS_CHANGES="false"
530 quiet="false"
531 local base_ref=""
532 while getopts "hb:p:j:l:iqv" opt; do
533 case $opt in
534 h)
535 print_usage
536 exit "$SUCCESS"
537 ;;
538 b)
539 base_ref="$OPTARG"
540 ;;
541 p)
542 past_ref="$OPTARG"
543 ;;
544 j)
545 MAX_THREADS="$OPTARG"
546 ;;
547 l)
548 abi_error_log="$OPTARG"
549 ;;
550 i)
551 IGNORE_AMBIGUOUS_CHANGES="true"
552 ;;
553 q)
554 quiet="true"
555 VERBOSE="false"
556 ;;
557 v)
558 VERBOSE="true"
559 quiet="false"
560 ;;
561 *)
562 exit "$FAIL_PREREQ"
563 esac
564 done
565
566 if [ "$quiet" = "true" ]; then
567 exec > /dev/null 2>&1
568 fi
569
570 run "$base_ref" "$past_ref" "$abi_error_log" "$@"
571 }
572
573 main "$@"