2 # SPDX-License-Identifier: GPL-2.0-only
3 # Script to check commits for UAPI backwards compatibility
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
20 Usage: $name [-b BASE_REF] [-p PAST_REF] [-j N] [-l ERROR_LOG] [-i] [-q] [-v]
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.
33 -v Verbose operation (print more information about each header being checked).
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)
42 $FAIL_ABI) ABI difference detected
43 $FAIL_PREREQ) Prerequisite not met
49 readonly FAIL_PREREQ
=2
53 # shellcheck disable=SC2059
57 # Expand an array with a specific character (similar to Python string.join())
64 # Create abidiff suppressions
66 # Common enum variant names which we don't want to worry about
67 # being shifted when new variants are added.
77 ".*_MAX_BPF_ATTACH_TYPE$"
82 ".*_NFT_META_IIFTYPE$"
94 # Common padding field names which can be expanded into
95 # without worrying about users.
96 local -a padding_regex
=(
118 changed_enumerators_regexp = $(join , "${enum_regex[@]}")
121 for p
in "${padding_regex[@]}"; do
125 has_data_member_inserted_at = offset_of_first_data_member_regexp(${p})
129 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ]; then
133 has_data_member_inserted_at = end
134 has_size_change = yes
139 # Check if git tree is dirty
144 # Get list of files installed in $ref
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
() {
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"
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
178 local -r inc_dir
="$1"
181 printf "int main(void) { return 0; }\n" | \
187 -fno-eliminate-unused-debug-types \
194 # Run make headers_install
195 run_make_headers_install
() {
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
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"
218 run_make_headers_install
"$ref"
219 add_to_incompat_list
"$ref" "$INCOMPAT_LIST"
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
230 printf "%s" "${TMP_DIR}/${ref}/usr"
233 # Check file list for UAPI compatibility
235 local -r base_ref
="$1"
236 local -r past_ref
="$2"
237 local -r abi_error_log
="$3"
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))
252 failed
=$
((failed
+ 1))
254 threads
=("${threads[@]:1}")
257 check_individual_file
"$base_ref" "$past_ref" "$file" &
259 done < <(get_file_list
"$past_ref")
261 for t
in "${threads[@]}"; do
263 passed
=$
((passed
+ 1))
265 failed
=$
((failed
+ 1))
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
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"
289 printf "All %d UAPI headers compatible with %s appear to be backwards compatible\n" \
296 # Check an individual file for UAPI compatibility
297 check_individual_file
() {
298 local -r base_ref
="$1"
299 local -r past_ref
="$2"
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"
313 compare_abi
"$file" "$base_header" "$past_header" "$base_ref" "$past_ref"
316 # Perform the A/B compilation and compare output ABI
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" \
332 printf "%s\n" "$warn_str"
334 printf -- "=%.0s" $
(seq 0 ${#warn_str})
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" \
343 printf "%s\n" "$warn_str"
345 printf -- "=%.0s" $
(seq 0 ${#warn_str})
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"
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"
366 if [ "$IGNORE_AMBIGUOUS_CHANGES" = "true" ] && [ "$ret" -eq 4 ]; then
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
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"
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
406 ABIDIFF
="${ABIDIFF:-abidiff}"
408 ARCH
="${ARCH:-$(uname -m)}"
409 if [ "$ARCH" = "x86_64" ]; then
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"
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"
431 if ! command -v "$CC" > /dev
/null
2>&1; then
432 eprintf
'error - %s not found\n' "$CC"
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"
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)"
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"
457 if ! git rev-parse
--verify "$past_ref" > /dev
/null
2>&1; then
458 printf 'error - invalid git reference "%s"\n' "$past_ref"
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"
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"
477 local abi_error_log
="$3"
480 if [ -z "$KERNEL_SRC" ]; then
481 KERNEL_SRC
="$(realpath "$
(dirname "$0")"/..)"
486 if [ -z "$base_ref" ] && ! tree_is_dirty
; then
490 if [ -z "$past_ref" ]; then
491 if [ -n "$base_ref" ]; then
492 past_ref
="${base_ref}^1"
498 if ! check_deps
; then
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}"
521 if ! check_uapi_files
"$base_ref" "$past_ref" "$abi_error_log"; then
529 IGNORE_AMBIGUOUS_CHANGES
="false"
532 while getopts "hb:p:j:l:iqv" opt
; do
545 MAX_THREADS
="$OPTARG"
548 abi_error_log
="$OPTARG"
551 IGNORE_AMBIGUOUS_CHANGES
="true"
566 if [ "$quiet" = "true" ]; then
567 exec > /dev
/null
2>&1
570 run
"$base_ref" "$past_ref" "$abi_error_log" "$@"