drm/imagination: Use the drm_sched_job_has_dependency helper
[drm/drm-misc.git] / scripts / check-uapi.sh
blob955581735cb3c371fc6ea7043bf8845911f837c3
1 #!/bin/bash
2 # SPDX-License-Identifier: GPL-2.0-only
3 # Script to check commits for UAPI backwards compatibility
5 set -o errexit
6 set -o pipefail
8 print_usage() {
9 name=$(basename "$0")
10 cat << EOF
11 $name - check for UAPI header stability across Git commits
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.
17 The script will not check UAPI headers for architectures other than the one
18 defined in ARCH.
20 Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
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).
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)
40 Exit codes:
41 $SUCCESS) Success
42 $FAIL_ABI) ABI difference detected
43 $FAIL_PREREQ) Prerequisite not met
44 EOF
47 readonly SUCCESS=0
48 readonly FAIL_ABI=1
49 readonly FAIL_PREREQ=2
51 # Print to stderr
52 eprintf() {
53 # shellcheck disable=SC2059
54 printf "$@" >&2
57 # Expand an array with a specific character (similar to Python string.join())
58 join() {
59 local IFS="$1"
60 shift
61 printf "%s" "$*"
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_.*"
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$"
115 cat << EOF
116 [suppress_type]
117 type_kind = enum
118 changed_enumerators_regexp = $(join , "${enum_regex[@]}")
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})
127 done
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
139 # Check if git tree is dirty
140 tree_is_dirty() {
141 ! git diff --quiet
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")"
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"
153 # Add to the list of incompatible headers
154 add_to_incompat_list() {
155 local -r ref="$1"
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"
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"
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"
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" \
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
202 # Install headers for both git refs
203 install_headers() {
204 local -r base_ref="$1"
205 local -r past_ref="$2"
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 -)
213 cd "${TMP_DIR}/${ref}-archive"
214 run_make_headers_install "$ref"
215 add_to_incompat_list "$ref" "$INCOMPAT_LIST"
217 else
218 run_make_headers_install "$ref"
219 add_to_incompat_list "$ref" "$INCOMPAT_LIST"
221 printf "OK\n"
222 done
223 sort -u -o "$INCOMPAT_LIST" "$INCOMPAT_LIST"
224 sed -i -e '/^$/d' "$INCOMPAT_LIST"
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"
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"
239 local passed=0;
240 local failed=0;
241 local -a threads=()
242 set -o errexit
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))
254 threads=("${threads[@]:1}")
257 check_individual_file "$base_ref" "$past_ref" "$file" &
258 threads+=("$!")
259 done < <(get_file_list "$past_ref")
261 for t in "${threads[@]}"; do
262 if wait "$t"; then
263 passed=$((passed + 1))
264 else
265 failed=$((failed + 1))
267 done
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"
274 while read -r error_file; do
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)
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"
288 else
289 printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
290 "$total" "$ARCH"
293 return "$failed"
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"
302 local -r base_header="$(get_header_tree "$base_ref")/${file}"
303 local -r past_header="$(get_header_tree "$past_ref")/${file}"
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
313 compare_abi "$file" "$base_header" "$past_header" "$base_ref" "$past_ref"
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"
326 mkdir -p "$(dirname "$log")"
328 if ! do_compile "$(get_header_tree "$base_ref")/include" "$base_header" "${base_header}.bin" 2> "$log"; then
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
339 if ! do_compile "$(get_header_tree "$past_ref")/include" "$past_header" "${past_header}.bin" 2> "$log"; then
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
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"
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
366 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
367 return 0
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
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"
389 } > "$error_log"
391 return 1
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"
400 printf "%s\n%s\n" "$min_version" "$version_installed" \
401 | sort -Vc > /dev/null 2>&1
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"
413 local -r abidiff_min_version="2.4"
414 local -r libdw_min_version_if_clang="0.171"
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
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
431 if ! command -v "$CC" > /dev/null 2>&1; then
432 eprintf 'error - %s not found\n' "$CC"
433 return 1
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
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
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
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
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
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
474 run() {
475 local base_ref="$1"
476 local past_ref="$2"
477 local abi_error_log="$3"
478 shift 3
480 if [ -z "$KERNEL_SRC" ]; then
481 KERNEL_SRC="$(realpath "$(dirname "$0")"/..)"
484 cd "$KERNEL_SRC"
486 if [ -z "$base_ref" ] && ! tree_is_dirty; then
487 base_ref=HEAD
490 if [ -z "$past_ref" ]; then
491 if [ -n "$base_ref" ]; then
492 past_ref="${base_ref}^1"
493 else
494 past_ref=HEAD
498 if ! check_deps; then
499 exit "$FAIL_PREREQ"
502 TMP_DIR=$(mktemp -d)
503 readonly TMP_DIR
504 trap 'rm -rf "$TMP_DIR"' EXIT
506 readonly INCOMPAT_LIST="${TMP_DIR}/incompat_list.txt"
507 touch "$INCOMPAT_LIST"
509 readonly SUPPRESSIONS="${TMP_DIR}/suppressions.txt"
510 gen_suppressions > "$SUPPRESSIONS"
512 # Run make install_headers for both refs
513 install_headers "$base_ref" "$past_ref"
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"
521 if ! check_uapi_files "$base_ref" "$past_ref" "$abi_error_log"; then
522 exit "$FAIL_ABI"
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
535 print_usage
536 exit "$SUCCESS"
539 base_ref="$OPTARG"
542 past_ref="$OPTARG"
545 MAX_THREADS="$OPTARG"
548 abi_error_log="$OPTARG"
551 IGNORE_AMBIGUOUS_CHANGES="true"
554 quiet="true"
555 VERBOSE="false"
558 VERBOSE="true"
559 quiet="false"
562 exit "$FAIL_PREREQ"
563 esac
564 done
566 if [ "$quiet" = "true" ]; then
567 exec > /dev/null 2>&1
570 run "$base_ref" "$past_ref" "$abi_error_log" "$@"
573 main "$@"