xref: /spdk/test/make/check_so_deps.sh (revision 34edd9f1bf5fda4c987f4500ddc3c9f50be32e7d)
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 = Added disable_command_passthru field
144			name = spdk_nvmf_transport_opts
145			soname_regexp = ^libspdk_nvmf\\.so\\.18\\.*$
146			has_data_member_regexp = ^reserved29$
147			has_data_member_inserted_between = {232, 256}
148		[suppress_type]
149			label = Added opts.disable_command_passthru field
150			name = spdk_nvmf_transport
151			soname_regexp = ^libspdk_nvmf\\.so\\.18\\.*$
152		[suppress_type]
153			label = Added disable_pcie_sgl_merge field
154			name = spdk_nvme_io_qpair_opts
155			soname_regexp = ^libspdk_nvme\\.so\\.13\\.*$
156			has_data_member_regexp = ^reserved66$
157			has_data_member_inserted_between = {528, end}
158	EOF
159
160	for object in "$libdir"/libspdk_*.so; do
161		abidiff_output=0
162
163		so_file=$(basename $object)
164		if [ ! -f "$source_abi_dir/$release/$so_file" ]; then
165			echo "No corresponding object for $so_file in canonical directory. Skipping."
166			continue
167		fi
168
169		cmd_args=('abidiff'
170			$source_abi_dir/$release/$so_file "$libdir/$so_file"
171			'--headers-dir1' $source_abi_dir/$release/include
172			'--headers-dir2' $rootdir/include
173			'--leaf-changes-only' '--suppressions' $suppression_file)
174
175		if ! output=$("${cmd_args[@]}" --stat); then
176			# remove any filtered out variables.
177			output=$(sed "s/ [()][^)]*[)]//g" <<< "$output")
178
179			IFS="." read -r _ _ new_so_maj new_so_min < <(readlink "$libdir/$so_file")
180			IFS="." read -r _ _ old_so_maj old_so_min < <(readlink "$source_abi_dir/$release/$so_file")
181
182			found_abi_change=false
183			so_name_changed=no
184
185			if [[ $output == *"ELF SONAME changed"* ]]; then
186				so_name_changed=yes
187			fi
188
189			changed_leaf_types=0
190			if [[ $output =~ "leaf types summary: "([0-9]+) ]]; then
191				changed_leaf_types=${BASH_REMATCH[1]}
192			fi
193
194			removed_functions=0 changed_functions=0 added_functions=0
195			if [[ $output =~ "functions summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
196				removed_functions=${BASH_REMATCH[1]} changed_functions=${BASH_REMATCH[2]} added_functions=${BASH_REMATCH[3]}
197			fi
198
199			removed_vars=0 changed_vars=0 added_vars=0
200			if [[ $output =~ "variables summary: "([0-9]+)" Removed, "([0-9]+)" Changed, "([0-9]+)" Added" ]]; then
201				removed_vars=${BASH_REMATCH[1]} changed_vars=${BASH_REMATCH[2]} added_vars=${BASH_REMATCH[3]}
202			fi
203
204			if ((changed_leaf_types != 0)); then
205				if ((new_so_maj == old_so_maj)); then
206					abidiff_output=1
207					abi_test_failed=true
208					echo "Please update the major SO version for $so_file. A header accessible type has been modified since last release."
209				fi
210				found_abi_change=true
211			fi
212
213			if ((removed_functions != 0)) || ((removed_vars != 0)); then
214				if ((new_so_maj == old_so_maj)); then
215					abidiff_output=1
216					abi_test_failed=true
217					echo "Please update the major SO version for $so_file. API functions or variables have been removed since last release."
218				fi
219				found_abi_change=true
220			fi
221
222			if ((changed_functions != 0)) || ((changed_vars != 0)); then
223				if ((new_so_maj == old_so_maj)); then
224					abidiff_output=1
225					abi_test_failed=true
226					echo "Please update the major SO version for $so_file. API functions or variables have been changed since last release."
227				fi
228				found_abi_change=true
229			fi
230
231			if ((added_functions != 0)) || ((added_vars != 0)); then
232				if ((new_so_min == old_so_min && new_so_maj == old_so_maj)) && ! $found_abi_change; then
233					abidiff_output=1
234					abi_test_failed=true
235					echo "Please update the minor SO version for $so_file. API functions or variables have been added since last release."
236				fi
237				found_abi_change=true
238			fi
239
240			if [[ $so_name_changed == yes ]]; then
241				# All SO major versions are intentionally increased after LTS to allow SO minor changes during the supported period.
242				if [[ "$release" == "$(get_release LTS).x" ]]; then
243					found_abi_change=true
244				fi
245				if ! $found_abi_change; then
246					echo "SO name for $so_file changed without a change to abi. please revert that change."
247					abi_test_failed=true
248				fi
249
250				if ((new_so_maj != old_so_maj && new_so_min != 0)); then
251					echo "SO major version for $so_file was bumped. Please reset the minor version to 0."
252					abi_test_failed=true
253				fi
254
255				if ((new_so_min > old_so_min + 1)); then
256					echo "SO minor version for $so_file was incremented more than once. Please revert minor version to $((old_so_min + 1))."
257					abi_test_failed=true
258				fi
259
260				if ((new_so_maj > old_so_maj + 1)); then
261					echo "SO major version for $so_file was incremented more than once. Please revert major version to $((old_so_maj + 1))."
262					abi_test_failed=true
263				fi
264			fi
265
266			if ((abidiff_output == 1)); then
267				"${cmd_args[@]}" --impacted-interfaces || :
268			fi
269		fi
270		processed_so=$((processed_so + 1))
271	done
272	rm -f $suppression_file
273	if [[ "$processed_so" -eq 0 ]]; then
274		echo "No shared objects were processed."
275		return 1
276	fi
277	echo "Processed $processed_so objects."
278	if [[ -z $user_abi_dir ]]; then
279		rm -rf "$source_abi_dir"
280	fi
281	if $abi_test_failed; then
282		echo "ERROR: ABI test failed"
283		exit 1
284	fi
285}
286
287list_deps_mk() {
288	local tab=$'\t'
289
290	make -f - <<- MAKE
291		SPDK_ROOT_DIR := $rootdir
292		include \$(SPDK_ROOT_DIR)/mk/spdk.common.mk
293		include \$(SPDK_ROOT_DIR)/mk/spdk.lib_deps.mk
294
295		all: \$(filter DEPDIRS-%,\$(.VARIABLES))
296
297		# Ignore any event_* dependencies. Those are based on the subsystem
298		# configuration and not readelf
299		DEPDIRS-%:
300			$tab@echo "\$(@:DEPDIRS-%=%)=\"\$(filter-out event_%,\$(\$@))\""
301	MAKE
302}
303
304function get_lib_shortname() {
305	local lib=${1##*/}
306	echo "${lib//@(libspdk_|.so)/}"
307}
308
309# shellcheck disable=SC2005
310sort_libs() { echo $(printf "%s\n" "$@" | sort); }
311
312function confirm_deps() {
313	local lib=$1 deplib lib_shortname symbols dep_names lib_make_deps found_symbol_lib
314
315	lib_shortname=$(get_lib_shortname "$lib")
316	lib_make_deps=(${!lib_shortname})
317
318	symbols=($(readelf -s --wide "$lib" | awk '$7 == "UND" {print $8}' | sort -u))
319	symbols_regx=$(
320		IFS="|"
321		echo "(${symbols[*]})"
322	)
323
324	if ((${#symbols[@]} > 0)); then
325		for deplib in "$libdir/"libspdk_!("$lib_shortname").so; do
326			readelf -s --wide "$deplib" | grep -m1 -qE "DEFAULT\s+[0-9]+\s$symbols_regx$" || continue
327			found_symbol_lib=$(get_lib_shortname "$deplib")
328			# Ignore the env_dpdk readelf dependency. We don't want people explicitly linking against it.
329			if [[ $found_symbol_lib != *env_dpdk* ]]; then
330				dep_names+=("$found_symbol_lib")
331			fi
332		done
333	fi
334
335	diff=$(echo "${dep_names[@]}" "${lib_make_deps[@]}" | tr ' ' '\n' | sort | uniq -u)
336	if [ "$diff" != "" ]; then
337		touch $fail_file
338		cat <<- MSG
339			There was a dependency mismatch in the library $lib_shortname
340			The makefile (spdk.lib_deps.mk) lists: '$(sort_libs "${lib_make_deps[@]}")'
341			readelf outputs:                       '$(sort_libs "${dep_names[@]}")'
342		MSG
343	fi
344}
345
346function confirm_makefile_deps() {
347	echo "---------------------------------------------------------------------"
348	# Exclude libspdk_env_dpdk.so from the library list. We don't link against this one so that
349	# users can define their own environment abstraction. However we do want to still check it
350	# for dependencies to avoid printing out a bunch of confusing symbols under the missing
351	# symbols section.
352	SPDK_LIBS=("$libdir/"libspdk_!(env_dpdk).so)
353	fail_file="$testdir/check_so_deps_fail"
354
355	rm -f $fail_file
356
357	declare -A IGNORED_LIBS=()
358	if grep -q 'CONFIG_RDMA?=n' $rootdir/mk/config.mk; then
359		IGNORED_LIBS["rdma"]=1
360	fi
361
362	(
363		source <(list_deps_mk)
364		for lib in "${SPDK_LIBS[@]}"; do confirm_deps "$lib" & done
365		wait
366	)
367
368	if [ -f $fail_file ]; then
369		rm -f $fail_file
370		echo "ERROR: Makefile deps test failed"
371		exit 1
372	fi
373}
374
375if [[ -e $config_file ]]; then
376	config_params=$(get_config_params)
377	if [[ "$SPDK_TEST_OCF" -eq 1 ]]; then
378		config_params="$config_params --with-ocf=$rootdir/ocf.a"
379	fi
380
381	if [[ -f $rootdir/mk/config.mk ]]; then
382		$MAKE $MAKEFLAGS clean
383	fi
384
385	$rootdir/configure $config_params --with-shared
386	$MAKE $MAKEFLAGS
387fi
388
389xtrace_disable
390
391run_test "check_header_filenames" check_header_filenames
392run_test "confirm_abi_deps" confirm_abi_deps
393run_test "confirm_makefile_deps" confirm_makefile_deps
394
395if [[ -e $config_file ]]; then
396	$MAKE $MAKEFLAGS clean
397fi
398
399xtrace_restore
400