~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

TOMOYO Linux Cross Reference
Linux/scripts/check-uapi.sh

Version: ~ [ linux-6.11.5 ] ~ [ linux-6.10.14 ] ~ [ linux-6.9.12 ] ~ [ linux-6.8.12 ] ~ [ linux-6.7.12 ] ~ [ linux-6.6.58 ] ~ [ linux-6.5.13 ] ~ [ linux-6.4.16 ] ~ [ linux-6.3.13 ] ~ [ linux-6.2.16 ] ~ [ linux-6.1.114 ] ~ [ linux-6.0.19 ] ~ [ linux-5.19.17 ] ~ [ linux-5.18.19 ] ~ [ linux-5.17.15 ] ~ [ linux-5.16.20 ] ~ [ linux-5.15.169 ] ~ [ linux-5.14.21 ] ~ [ linux-5.13.19 ] ~ [ linux-5.12.19 ] ~ [ linux-5.11.22 ] ~ [ linux-5.10.228 ] ~ [ linux-5.9.16 ] ~ [ linux-5.8.18 ] ~ [ linux-5.7.19 ] ~ [ linux-5.6.19 ] ~ [ linux-5.5.19 ] ~ [ linux-5.4.284 ] ~ [ linux-5.3.18 ] ~ [ linux-5.2.21 ] ~ [ linux-5.1.21 ] ~ [ linux-5.0.21 ] ~ [ linux-4.20.17 ] ~ [ linux-4.19.322 ] ~ [ linux-4.18.20 ] ~ [ linux-4.17.19 ] ~ [ linux-4.16.18 ] ~ [ linux-4.15.18 ] ~ [ linux-4.14.336 ] ~ [ linux-4.13.16 ] ~ [ linux-4.12.14 ] ~ [ linux-4.11.12 ] ~ [ linux-4.10.17 ] ~ [ linux-4.9.337 ] ~ [ linux-4.4.302 ] ~ [ linux-3.10.108 ] ~ [ linux-2.6.32.71 ] ~ [ linux-2.6.0 ] ~ [ linux-2.4.37.11 ] ~ [ unix-v6-master ] ~ [ ccs-tools-1.8.9 ] ~ [ policy-sample ] ~
Architecture: ~ [ i386 ] ~ [ alpha ] ~ [ m68k ] ~ [ mips ] ~ [ ppc ] ~ [ sparc ] ~ [ sparc64 ] ~

  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 "$@"

~ [ source navigation ] ~ [ diff markup ] ~ [ identifier search ] ~

kernel.org | git.kernel.org | LWN.net | Project Home | SVN repository | Mail admin

Linux® is a registered trademark of Linus Torvalds in the United States and other countries.
TOMOYO® is a registered trademark of NTT DATA CORPORATION.

sflogo.php