xref: /spdk/test/make/check_so_deps.sh (revision 671ee5e075565cae5d7a23767c887600496be6f0)
1#!/usr/bin/env bash
2#  SPDX-License-Identifier: BSD-3-Clause
3#  Copyright (C) 2019 Intel Corporation
4#  All rights reserved.
5#
6shopt -s extglob
7
8if [ "$(uname -s)" = "FreeBSD" ]; then
9	echo "Not testing for shared object dependencies on FreeBSD."
10	exit 1
11fi
12
13testdir=$(readlink -f $(dirname $0))
14rootdir=$(readlink -f $testdir/../..)
15
16function usage() {
17	script_name="$(basename $0)"
18	echo "Usage: $script_name"
19	echo "    -c, --config-file     Rebuilds SPDK according to config file from autotest"
20	echo "    -a, --spdk-abi-path   Use spdk-abi from specified path, otherwise"
21	echo "                          latest version is pulled and deleted after test"
22	echo "        --release=RELEASE Compare ABI against RELEASE"
23	echo "    -h, --help            Print this help"
24	echo "Example:"
25	echo "$script_name -c ./autotest.config -a /path/to/spdk-abi"
26}
27
28# Parse input arguments #
29while getopts 'hc:a:-:' optchar; do
30	case "$optchar" in
31		-)
32			case "$OPTARG" in
33				help)
34					usage
35					exit 0
36					;;
37				config-file=*)
38					config_file="$(readlink -f ${OPTARG#*=})"
39					;;
40				spdk-abi-path=*)
41					user_abi_dir="$(readlink -f ${OPTARG#*=})"
42					;;
43				release=*)
44					RELEASE="${OPTARG#*=}"
45					;;
46				*) exit 1 ;;
47			esac
48			;;
49		h)
50			usage
51			exit 0
52			;;
53		c) config_file="$(readlink -f ${OPTARG#*=})" ;;
54		a) user_abi_dir="$(readlink -f ${OPTARG#*=})" ;;
55		*) exit 1 ;;
56	esac
57done
58
59source "$rootdir/test/common/autotest_common.sh"
60
61if [[ -e $config_file ]]; then
62	source "$config_file"
63fi
64
65source_abi_dir="${user_abi_dir:-"$testdir/abi"}"
66libdir="$rootdir/build/lib"
67libdeps_file="$rootdir/mk/spdk.lib_deps.mk"
68
69function check_header_filenames() {
70	local dups_found=0
71
72	include_headers=$(git ls-files -- $rootdir/include/spdk $rootdir/include/spdk_internal | xargs -n 1 basename)
73	dups=
74	for file in $include_headers; do
75		if [[ $(git ls-files "$rootdir/lib/**/$file" "$rootdir/module/**/$file" --error-unmatch 2> /dev/null) ]]; then
76			dups+=" $file"
77			dups_found=1
78		fi
79	done
80
81	if ((dups_found == 1)); then
82		echo "Private header file(s) found with same name as public header file."
83		echo "This is not allowed since it can confuse abidiff when determining if"
84		echo "data structure changes impact ABI."
85		echo $dups
86		return 1
87	fi
88}
89
90function get_release() {
91	local tag
92
93	tag=$(git describe --tags --abbrev=0 --exclude=LTS --exclude="*-pre" $1)
94	echo "${tag:0:6}"
95}
96
97function confirm_abi_deps() {
98	local processed_so=0
99	local abi_test_failed=false
100	local abidiff_output
101	local release
102	local suppression_file="$testdir/abigail_suppressions.ini"
103
104	release="${RELEASE:-$(get_release)}.x"
105
106	if [[ ! -d $source_abi_dir ]]; then
107		mkdir -p $source_abi_dir
108		echo "spdk-abi has not been found at $source_abi_dir, cloning"
109		git clone "https://github.com/spdk/spdk-abi.git" "$source_abi_dir"
110	fi
111
112	if [[ ! -d "$source_abi_dir/$release" ]]; then
113		echo "Release (${release%.*}) does not exist in spdk-abi repository"
114		return 1
115	fi
116
117	echo "* Running ${FUNCNAME[0]} against the ${release%.*} release" >&2
118
119	if ! hash abidiff; then
120		echo "Unable to check ABI compatibility. Please install abidiff."
121		return 1
122	fi
123
124	# Type suppression should be used for deliberate change in the structure,
125	# that do not affect the ABI compatibility.
126	# One example is utilizing a previously reserved field in the structure.
127	# Suppression file should contain at the very least the name of type
128	# and point to major SO_VER of the library it applies to.
129	#
130	# To avoid ignoring an actual changes in ABI for a particular SO_VER,
131	# the suppression should be as specific as possible to the change.
132	# has_data_member_* conditions can be used to narrow down the
133	# name of previously existing field, or specify the offset.
134	# Please refer to libabigail for details.
135	#
136	# Example format:
137	# [suppress_type]
138	#	label = Added interrupt_mode field
139	#	name = spdk_app_opts
140	#	soname_regexp = ^libspdk_event\\.so\\.12\\.*$
141	cat <<- EOF > ${suppression_file}
142		[suppress_type]
143			label = Removed spdk_nvme_accel_fn_table.submit_accel_crc32c field
144			name = spdk_nvme_accel_fn_table
145			soname_regexp = ^libspdk_nvme\\.so\\.14\\.*$
146			has_data_member_regexp = ^submit_accel_crc32c$
147			has_data_member_inserted_between = {64, 128}
148	EOF
149
150	for object in "$libdir"/libspdk_*.so; do
151		abidiff_output=0
152
153		so_file=$(basename $object)
154		if [ ! -f "$source_abi_dir/$release/$so_file" ]; then
155			echo "No corresponding object for $so_file in canonical directory. Skipping."
156			continue
157		fi
158
159		cmd_args=('abidiff'
160			$source_abi_dir/$release/$so_file "$libdir/$so_file"
161			'--headers-dir1' $source_abi_dir/$release/include
162			'--headers-dir2' $rootdir/include
163			'--leaf-changes-only' '--suppressions' $suppression_file)
164
165		if ! output=$("${cmd_args[@]}" --stat); then
166			# remove any filtered out variables.
167			output=$(sed "s/ [()][^)]*[)]//g" <<< "$output")
168
169			IFS="." read -r _ _ new_so_maj new_so_min < <(readlink "$libdir/$so_file")
170			IFS="." read -r _ _ old_so_maj old_so_min < <(readlink "$source_abi_dir/$release/$so_file")
171
172			found_abi_change=false
173			so_name_changed=no
174
175			if [[ $output == *"ELF SONAME changed"* ]]; then
176				so_name_changed=yes
177			fi
178
179			changed_leaf_types=0
180			if [[ $output =~ "leaf types summary: "([0-9]+) ]]; then
181				changed_leaf_types=${BASH_REMATCH[1]}
182			fi
183
184			removed_functions=0 changed_functions=0 added_functions=0
185			if [[ $output =~ "functions summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
186				removed_functions=${BASH_REMATCH[1]} changed_functions=${BASH_REMATCH[2]} added_functions=${BASH_REMATCH[3]}
187			fi
188
189			removed_vars=0 changed_vars=0 added_vars=0
190			if [[ $output =~ "variables summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
191				removed_vars=${BASH_REMATCH[1]} changed_vars=${BASH_REMATCH[2]} added_vars=${BASH_REMATCH[3]}
192			fi
193
194			if ((changed_leaf_types != 0)); then
195				if ((new_so_maj == old_so_maj)); then
196					abidiff_output=1
197					abi_test_failed=true
198					echo "Please update the major SO version for $so_file. A header accessible type has been modified since last release."
199				fi
200				found_abi_change=true
201			fi
202
203			if ((removed_functions != 0)) || ((removed_vars != 0)); then
204				if ((new_so_maj == old_so_maj)); then
205					abidiff_output=1
206					abi_test_failed=true
207					echo "Please update the major SO version for $so_file. API functions or variables have been removed since last release."
208				fi
209				found_abi_change=true
210			fi
211
212			if ((changed_functions != 0)) || ((changed_vars != 0)); then
213				if ((new_so_maj == old_so_maj)); then
214					abidiff_output=1
215					abi_test_failed=true
216					echo "Please update the major SO version for $so_file. API functions or variables have been changed since last release."
217				fi
218				found_abi_change=true
219			fi
220
221			if ((added_functions != 0)) || ((added_vars != 0)); then
222				if ((new_so_min == old_so_min && new_so_maj == old_so_maj)) && ! $found_abi_change; then
223					abidiff_output=1
224					abi_test_failed=true
225					echo "Please update the minor SO version for $so_file. API functions or variables have been added since last release."
226				fi
227				found_abi_change=true
228			fi
229
230			if [[ $so_name_changed == yes ]]; then
231				# All SO major versions are intentionally increased after LTS to allow SO minor changes during the supported period.
232				if [[ "$release" == "$(get_release LTS).x" ]]; then
233					found_abi_change=true
234				fi
235				if ! $found_abi_change; then
236					echo "SO name for $so_file changed without a change to abi. please revert that change."
237					abi_test_failed=true
238				fi
239
240				if ((new_so_maj != old_so_maj && new_so_min != 0)); then
241					echo "SO major version for $so_file was bumped. Please reset the minor version to 0."
242					abi_test_failed=true
243				fi
244
245				if ((new_so_min > old_so_min + 1)); then
246					echo "SO minor version for $so_file was incremented more than once. Please revert minor version to $((old_so_min + 1))."
247					abi_test_failed=true
248				fi
249
250				if ((new_so_maj > old_so_maj + 1)); then
251					echo "SO major version for $so_file was incremented more than once. Please revert major version to $((old_so_maj + 1))."
252					abi_test_failed=true
253				fi
254			fi
255
256			if ((abidiff_output == 1)); then
257				"${cmd_args[@]}" --impacted-interfaces || :
258			fi
259		fi
260		processed_so=$((processed_so + 1))
261	done
262	rm -f $suppression_file
263	if [[ "$processed_so" -eq 0 ]]; then
264		echo "No shared objects were processed."
265		return 1
266	fi
267	echo "Processed $processed_so objects."
268	if [[ -z $user_abi_dir ]]; then
269		rm -rf "$source_abi_dir"
270	fi
271	if $abi_test_failed; then
272		echo "ERROR: ABI test failed"
273		exit 1
274	fi
275}
276
277list_deps_mk() {
278	local tab=$'\t'
279
280	make -f - <<- MAKE
281		SPDK_ROOT_DIR := $rootdir
282		include \$(SPDK_ROOT_DIR)/mk/spdk.common.mk
283		include \$(SPDK_ROOT_DIR)/mk/spdk.lib_deps.mk
284
285		all: \$(filter DEPDIRS-%,\$(.VARIABLES))
286
287		# Ignore any event_* dependencies. Those are based on the subsystem
288		# configuration and not readelf
289		DEPDIRS-%:
290			$tab@echo "\$(@:DEPDIRS-%=%)=\"\$(filter-out event_%,\$(\$@))\""
291	MAKE
292}
293
294function get_lib_shortname() {
295	local lib=${1##*/}
296	echo "${lib//@(libspdk_|.so)/}"
297}
298
299# shellcheck disable=SC2005
300sort_libs() { echo $(printf "%s\n" "$@" | sort); }
301
302function confirm_deps() {
303	local lib=$1 deplib lib_shortname symbols dep_names lib_make_deps found_symbol_lib
304
305	lib_shortname=$(get_lib_shortname "$lib")
306	lib_make_deps=(${!lib_shortname})
307
308	symbols=($(readelf -s --wide "$lib" | awk '$7 == "UND" {print $8}' | sort -u))
309	symbols_regx=$(
310		IFS="|"
311		echo "(${symbols[*]})"
312	)
313
314	if ((${#symbols[@]} > 0)); then
315		for deplib in "$libdir/"libspdk_!("$lib_shortname").so; do
316			readelf -s --wide "$deplib" | grep -m1 -qE "DEFAULT\s+[0-9]+\s$symbols_regx$" || continue
317			found_symbol_lib=$(get_lib_shortname "$deplib")
318			# Ignore the env_dpdk readelf dependency. We don't want people explicitly linking against it.
319			if [[ $found_symbol_lib != *env_dpdk* ]]; then
320				dep_names+=("$found_symbol_lib")
321			fi
322		done
323	fi
324
325	diff=$(echo "${dep_names[@]}" "${lib_make_deps[@]}" | tr ' ' '\n' | sort | uniq -u)
326	if [ "$diff" != "" ]; then
327		touch $fail_file
328		cat <<- MSG
329			There was a dependency mismatch in the library $lib_shortname
330			The makefile (spdk.lib_deps.mk) lists: '$(sort_libs "${lib_make_deps[@]}")'
331			readelf outputs:                       '$(sort_libs "${dep_names[@]}")'
332		MSG
333	fi
334}
335
336function confirm_makefile_deps() {
337	echo "---------------------------------------------------------------------"
338	# Exclude libspdk_env_dpdk.so from the library list. We don't link against this one so that
339	# users can define their own environment abstraction. However we do want to still check it
340	# for dependencies to avoid printing out a bunch of confusing symbols under the missing
341	# symbols section.
342	SPDK_LIBS=("$libdir/"libspdk_!(env_dpdk).so)
343	fail_file="$testdir/check_so_deps_fail"
344
345	rm -f $fail_file
346
347	declare -A IGNORED_LIBS=()
348	if [[ $CONFIG_RDMA == n ]]; then
349		IGNORED_LIBS["rdma"]=1
350	fi
351
352	(
353		source <(list_deps_mk)
354		for lib in "${SPDK_LIBS[@]}"; do confirm_deps "$lib" & done
355		wait
356	)
357
358	if [ -f $fail_file ]; then
359		rm -f $fail_file
360		echo "ERROR: Makefile deps test failed"
361		exit 1
362	fi
363}
364
365if [[ -e $config_file ]]; then
366	config_params=$(get_config_params)
367	if [[ "$SPDK_TEST_OCF" -eq 1 ]]; then
368		config_params="$config_params --with-ocf=$rootdir/ocf.a"
369	fi
370
371	if [[ -f $rootdir/mk/config.mk ]]; then
372		$MAKE $MAKEFLAGS clean
373	fi
374
375	$rootdir/configure $config_params --with-shared
376	$MAKE $MAKEFLAGS
377fi
378
379xtrace_disable
380
381run_test "check_header_filenames" check_header_filenames
382run_test "confirm_abi_deps" confirm_abi_deps
383run_test "confirm_makefile_deps" confirm_makefile_deps
384
385if [[ -e $config_file ]]; then
386	$MAKE $MAKEFLAGS clean
387fi
388
389xtrace_restore
390