]>
Commit | Line | Data |
---|---|---|
1f7f31bf JM |
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 "$@" |