xref: /spdk/test/make/check_so_deps.sh (revision 2104eacf0c136776cfdaa3ea9c187a7522b3ede0)
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 version major minor patch suffix
92
93	if [[ -n "$1" ]]; then
94		version="$1"
95	else
96		IFS='.-' read -r major minor patch suffix < "$rootdir/VERSION"
97		version="v$major.$minor"
98		((patch > 0)) && version+=".$patch"
99		version+=${suffix:+-$suffix}
100	fi
101
102	tag=$(git describe --tags --abbrev=0 --exclude=LTS --exclude="*-pre" "$version")
103	echo "${tag:0:6}"
104}
105
106function confirm_abi_deps() {
107	local processed_so=0
108	local abi_test_failed=false
109	local abidiff_output
110	local release
111	local suppression_file="$testdir/abigail_suppressions.ini"
112
113	release="${RELEASE:-$(get_release)}.x"
114
115	if [[ ! -d $source_abi_dir ]]; then
116		mkdir -p $source_abi_dir
117		echo "spdk-abi has not been found at $source_abi_dir, cloning"
118		git clone "https://github.com/spdk/spdk-abi.git" "$source_abi_dir"
119	fi
120
121	if [[ ! -d "$source_abi_dir/$release" ]]; then
122		echo "Release (${release%.*}) does not exist in spdk-abi repository"
123		return 1
124	fi
125
126	echo "* Running ${FUNCNAME[0]} against the ${release%.*} release" >&2
127
128	if ! hash abidiff; then
129		echo "Unable to check ABI compatibility. Please install abidiff."
130		return 1
131	fi
132
133	# Type suppression should be used for deliberate change in the structure,
134	# that do not affect the ABI compatibility.
135	# One example is utilizing a previously reserved field in the structure.
136	# Suppression file should contain at the very least the name of type
137	# and point to major SO_VER of the library it applies to.
138	#
139	# To avoid ignoring an actual changes in ABI for a particular SO_VER,
140	# the suppression should be as specific as possible to the change.
141	# has_data_member_* conditions can be used to narrow down the
142	# name of previously existing field, or specify the offset.
143	# Please refer to libabigail for details.
144	#
145	# Example format:
146	# [suppress_type]
147	#	label = Added interrupt_mode field
148	#	name = spdk_app_opts
149	#	soname_regexp = ^libspdk_event\\.so\\.12\\.*$
150	cat <<- EOF > ${suppression_file}
151		[suppress_type]
152			label = Removed spdk_nvme_accel_fn_table.submit_accel_crc32c field
153			name = spdk_nvme_accel_fn_table
154			soname_regexp = ^libspdk_nvme\\.so\\.14\\.*$
155			has_data_member_regexp = ^submit_accel_crc32c$
156			has_data_member_inserted_between = {64, 128}
157	EOF
158
159	for object in "$libdir"/libspdk_*.so; do
160		abidiff_output=0
161
162		so_file=$(basename $object)
163		if [ ! -f "$source_abi_dir/$release/$so_file" ]; then
164			echo "No corresponding object for $so_file in canonical directory. Skipping."
165			continue
166		fi
167
168		cmd_args=('abidiff'
169			$source_abi_dir/$release/$so_file "$libdir/$so_file"
170			'--headers-dir1' $source_abi_dir/$release/include
171			'--headers-dir2' $rootdir/include
172			'--leaf-changes-only' '--suppressions' $suppression_file)
173
174		if ! output=$("${cmd_args[@]}" --stat); then
175			# remove any filtered out variables.
176			output=$(sed "s/ [()][^)]*[)]//g" <<< "$output")
177
178			IFS="." read -r _ _ new_so_maj new_so_min < <(readlink "$libdir/$so_file")
179			IFS="." read -r _ _ old_so_maj old_so_min < <(readlink "$source_abi_dir/$release/$so_file")
180
181			found_abi_change=false
182			so_name_changed=no
183
184			if [[ $output == *"ELF SONAME changed"* ]]; then
185				so_name_changed=yes
186			fi
187
188			changed_leaf_types=0
189			if [[ $output =~ "leaf types summary: "([0-9]+) ]]; then
190				changed_leaf_types=${BASH_REMATCH[1]}
191			fi
192
193			removed_functions=0 changed_functions=0 added_functions=0
194			if [[ $output =~ "functions summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
195				removed_functions=${BASH_REMATCH[1]} changed_functions=${BASH_REMATCH[2]} added_functions=${BASH_REMATCH[3]}
196			fi
197
198			removed_vars=0 changed_vars=0 added_vars=0
199			if [[ $output =~ "variables summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
200				removed_vars=${BASH_REMATCH[1]} changed_vars=${BASH_REMATCH[2]} added_vars=${BASH_REMATCH[3]}
201			fi
202
203			if ((changed_leaf_types != 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. A header accessible type has been modified since last release."
208				fi
209				found_abi_change=true
210			fi
211
212			if ((removed_functions != 0)) || ((removed_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 removed since last release."
217				fi
218				found_abi_change=true
219			fi
220
221			if ((changed_functions != 0)) || ((changed_vars != 0)); then
222				if ((new_so_maj == old_so_maj)); then
223					abidiff_output=1
224					abi_test_failed=true
225					echo "Please update the major SO version for $so_file. API functions or variables have been changed since last release."
226				fi
227				found_abi_change=true
228			fi
229
230			if ((added_functions != 0)) || ((added_vars != 0)); then
231				if ((new_so_min == old_so_min && new_so_maj == old_so_maj)) && ! $found_abi_change; then
232					abidiff_output=1
233					abi_test_failed=true
234					echo "Please update the minor SO version for $so_file. API functions or variables have been added since last release."
235				fi
236				found_abi_change=true
237			fi
238
239			if [[ $so_name_changed == yes ]]; then
240				# All SO major versions are intentionally increased after LTS to allow SO minor changes during the supported period.
241				if [[ "$release" == "$(get_release LTS).x" ]]; then
242					found_abi_change=true
243				fi
244				if ! $found_abi_change; then
245					echo "SO name for $so_file changed without a change to abi. please revert that change."
246					abi_test_failed=true
247				fi
248
249				if ((new_so_maj != old_so_maj && new_so_min != 0)); then
250					echo "SO major version for $so_file was bumped. Please reset the minor version to 0."
251					abi_test_failed=true
252				fi
253
254				if ((new_so_min > old_so_min + 1)); then
255					echo "SO minor version for $so_file was incremented more than once. Please revert minor version to $((old_so_min + 1))."
256					abi_test_failed=true
257				fi
258
259				if ((new_so_maj > old_so_maj + 1)); then
260					echo "SO major version for $so_file was incremented more than once. Please revert major version to $((old_so_maj + 1))."
261					abi_test_failed=true
262				fi
263			fi
264
265			if ((abidiff_output == 1)); then
266				"${cmd_args[@]}" --impacted-interfaces || :
267			fi
268		fi
269		processed_so=$((processed_so + 1))
270	done
271	rm -f $suppression_file
272	if [[ "$processed_so" -eq 0 ]]; then
273		echo "No shared objects were processed."
274		return 1
275	fi
276	echo "Processed $processed_so objects."
277	if [[ -z $user_abi_dir ]]; then
278		rm -rf "$source_abi_dir"
279	fi
280	if $abi_test_failed; then
281		echo "ERROR: ABI test failed"
282		exit 1
283	fi
284}
285
286list_deps_mk() {
287	local tab=$'\t'
288
289	make -f - <<- MAKE
290		SPDK_ROOT_DIR := $rootdir
291		include \$(SPDK_ROOT_DIR)/mk/spdk.common.mk
292		include \$(SPDK_ROOT_DIR)/mk/spdk.lib_deps.mk
293
294		all: \$(filter DEPDIRS-%,\$(.VARIABLES))
295
296		# Ignore any event_* dependencies. Those are based on the subsystem
297		# configuration and not readelf
298		DEPDIRS-%:
299			$tab@echo "\$(@:DEPDIRS-%=%)=\"\$(filter-out event_%,\$(\$@))\""
300	MAKE
301}
302
303function get_lib_shortname() {
304	local lib=${1##*/}
305	echo "${lib//@(libspdk_|.so)/}"
306}
307
308# shellcheck disable=SC2005
309sort_libs() { echo $(printf "%s\n" "$@" | sort); }
310
311function confirm_deps() {
312	local lib=$1 deplib lib_shortname symbols dep_names lib_make_deps found_symbol_lib
313
314	lib_shortname=$(get_lib_shortname "$lib")
315	lib_make_deps=(${!lib_shortname})
316
317	symbols=($(readelf -s --wide "$lib" | awk '$7 == "UND" {print $8}' | sort -u))
318	symbols_regx=$(
319		IFS="|"
320		echo "(${symbols[*]})"
321	)
322
323	if ((${#symbols[@]} > 0)); then
324		for deplib in "$libdir/"libspdk_!("$lib_shortname").so; do
325			readelf -s --wide "$deplib" | grep -m1 -qE "DEFAULT\s+[0-9]+\s$symbols_regx$" || continue
326			found_symbol_lib=$(get_lib_shortname "$deplib")
327			# Ignore the env_dpdk readelf dependency. We don't want people explicitly linking against it.
328			if [[ $found_symbol_lib != *env_dpdk* ]]; then
329				dep_names+=("$found_symbol_lib")
330			fi
331		done
332	fi
333
334	diff=$(echo "${dep_names[@]}" "${lib_make_deps[@]}" | tr ' ' '\n' | sort | uniq -u)
335	if [ "$diff" != "" ]; then
336		touch $fail_file
337		cat <<- MSG
338			There was a dependency mismatch in the library $lib_shortname
339			The makefile (spdk.lib_deps.mk) lists: '$(sort_libs "${lib_make_deps[@]}")'
340			readelf outputs:                       '$(sort_libs "${dep_names[@]}")'
341		MSG
342	fi
343}
344
345function confirm_makefile_deps() {
346	echo "---------------------------------------------------------------------"
347	# Exclude libspdk_env_dpdk.so from the library list. We don't link against this one so that
348	# users can define their own environment abstraction. However we do want to still check it
349	# for dependencies to avoid printing out a bunch of confusing symbols under the missing
350	# symbols section.
351	SPDK_LIBS=("$libdir/"libspdk_!(env_dpdk).so)
352	fail_file="$testdir/check_so_deps_fail"
353
354	rm -f $fail_file
355
356	declare -A IGNORED_LIBS=()
357	if [[ $CONFIG_RDMA == n ]]; then
358		IGNORED_LIBS["rdma"]=1
359	fi
360
361	(
362		source <(list_deps_mk)
363		for lib in "${SPDK_LIBS[@]}"; do confirm_deps "$lib" & done
364		wait
365	)
366
367	if [ -f $fail_file ]; then
368		rm -f $fail_file
369		echo "ERROR: Makefile deps test failed"
370		exit 1
371	fi
372}
373
374if [[ -e $config_file ]]; then
375	config_params=$(get_config_params)
376	if [[ "$SPDK_TEST_OCF" -eq 1 ]]; then
377		config_params="$config_params --with-ocf=$rootdir/ocf.a"
378	fi
379
380	if [[ -f $rootdir/mk/config.mk ]]; then
381		$MAKE $MAKEFLAGS clean
382	fi
383
384	$rootdir/configure $config_params --with-shared
385	$MAKE $MAKEFLAGS
386fi
387
388xtrace_disable
389
390run_test "check_header_filenames" check_header_filenames
391run_test "confirm_abi_deps" confirm_abi_deps
392run_test "confirm_makefile_deps" confirm_makefile_deps
393
394if [[ -e $config_file ]]; then
395	$MAKE $MAKEFLAGS clean
396fi
397
398xtrace_restore
399