Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
torvalds
GitHub Repository: torvalds/linux
Path: blob/master/scripts/check-uapi.sh
26242 views
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 "$@"
574
575