Skip to content

Commit ffa27af

Browse files
committed
test: Add check-deps.sh script to check for unexpected library dependencies
1 parent 2f53f22 commit ffa27af

File tree

1 file changed

+203
-0
lines changed

1 file changed

+203
-0
lines changed

contrib/devtools/check-deps.sh

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
#!/usr/bin/env bash
2+
3+
export LC_ALL=C
4+
set -Eeuo pipefail
5+
6+
# Declare paths to libraries
7+
declare -A LIBS
8+
LIBS[cli]="libbitcoin_cli.a"
9+
LIBS[common]="libbitcoin_common.a"
10+
LIBS[consensus]="libbitcoin_consensus.a"
11+
LIBS[crypto]="crypto/.libs/libbitcoin_crypto_base.a crypto/.libs/libbitcoin_crypto_x86_shani.a crypto/.libs/libbitcoin_crypto_sse41.a crypto/.libs/libbitcoin_crypto_avx2.a"
12+
LIBS[node]="libbitcoin_node.a"
13+
LIBS[util]="libbitcoin_util.a"
14+
LIBS[wallet]="libbitcoin_wallet.a"
15+
LIBS[wallet_tool]="libbitcoin_wallet_tool.a"
16+
17+
# Declare allowed dependencies "X Y" where X is allowed to depend on Y. This
18+
# list is taken from doc/design/libraries.md.
19+
ALLOWED_DEPENDENCIES=(
20+
"cli common"
21+
"cli util"
22+
"common consensus"
23+
"common crypto"
24+
"common util"
25+
"consensus crypto"
26+
"node common"
27+
"node consensus"
28+
"node crypto"
29+
"node kernel"
30+
"node util"
31+
"util crypto"
32+
"wallet common"
33+
"wallet crypto"
34+
"wallet util"
35+
"wallet_tool util"
36+
"wallet_tool wallet"
37+
)
38+
39+
# Add minor dependencies omitted from doc/design/libraries.md to keep the
40+
# dependency diagram simple.
41+
ALLOWED_DEPENDENCIES+=(
42+
"wallet consensus"
43+
"wallet_tool common"
44+
"wallet_tool crypto"
45+
)
46+
47+
# Declare list of known errors that should be suppressed.
48+
declare -A SUPPRESS
49+
# init.cpp file currently calls Berkeley DB sanity check function on startup, so
50+
# there is an undocumented dependency of the node library on the wallet library.
51+
SUPPRESS["libbitcoin_node_a-init.o libbitcoin_wallet_a-bdb.o _ZN6wallet27BerkeleyDatabaseSanityCheckEv"]=1
52+
# init/common.cpp file calls InitError and InitWarning from interface_ui which
53+
# is currently part of the node library. interface_ui should just be part of the
54+
# common library instead, and is moved in
55+
# https://github.com/bitcoin/bitcoin/issues/10102
56+
SUPPRESS["libbitcoin_common_a-common.o libbitcoin_node_a-interface_ui.o _Z11InitWarningRK13bilingual_str"]=1
57+
SUPPRESS["libbitcoin_common_a-common.o libbitcoin_node_a-interface_ui.o _Z9InitErrorRK13bilingual_str"]=1
58+
# rpc/external_signer.cpp adds defines node RPC methods but is built as part of the
59+
# common library. It should be moved to the node library instead.
60+
SUPPRESS["libbitcoin_common_a-external_signer.o libbitcoin_node_a-server.o _ZN9CRPCTable13appendCommandERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPK11CRPCCommand"]=1
61+
62+
usage() {
63+
echo "Usage: $(basename "${BASH_SOURCE[0]}") [BUILD_DIR]"
64+
}
65+
66+
# Output makefile targets, converting library .a paths to libtool .la targets
67+
lib_targets() {
68+
for lib in "${!LIBS[@]}"; do
69+
for lib_path in ${LIBS[$lib]}; do
70+
# shellcheck disable=SC2001
71+
sed 's:/.libs/\(.*\)\.a$:/\1.la:g' <<<"$lib_path"
72+
done
73+
done
74+
}
75+
76+
# Extract symbol names and object names and write to text files
77+
extract_symbols() {
78+
local temp_dir="$1"
79+
for lib in "${!LIBS[@]}"; do
80+
for lib_path in ${LIBS[$lib]}; do
81+
nm -o "$lib_path" | grep ' T ' | awk '{print $3, $1}' >> "${temp_dir}/${lib}_exports.txt"
82+
nm -o "$lib_path" | grep ' U ' | awk '{print $3, $1}' >> "${temp_dir}/${lib}_imports.txt"
83+
awk '{print $1}' "${temp_dir}/${lib}_exports.txt" | sort -u > "${temp_dir}/${lib}_exported_symbols.txt"
84+
awk '{print $1}' "${temp_dir}/${lib}_imports.txt" | sort -u > "${temp_dir}/${lib}_imported_symbols.txt"
85+
done
86+
done
87+
}
88+
89+
# Lookup object name(s) corresponding to symbol name in text file
90+
obj_names() {
91+
local symbol="$1"
92+
local txt_file="$2"
93+
sed -n "s/^$symbol [^:]\\+:\\([^:]\\+\\):[^:]*\$/\\1/p" "$txt_file" | sort -u
94+
}
95+
96+
# Iterate through libraries and find disallowed dependencies
97+
check_libraries() {
98+
local temp_dir="$1"
99+
local result=0
100+
for src in "${!LIBS[@]}"; do
101+
for dst in "${!LIBS[@]}"; do
102+
if [ "$src" != "$dst" ] && ! is_allowed "$src" "$dst"; then
103+
if ! check_disallowed "$src" "$dst" "$temp_dir"; then
104+
result=1
105+
fi
106+
fi
107+
done
108+
done
109+
check_not_suppressed
110+
return $result
111+
}
112+
113+
# Return whether src library is allowed to depend on dst.
114+
is_allowed() {
115+
local src="$1"
116+
local dst="$2"
117+
for allowed in "${ALLOWED_DEPENDENCIES[@]}"; do
118+
if [ "$src $dst" = "$allowed" ]; then
119+
return 0
120+
fi
121+
done
122+
return 1
123+
}
124+
125+
# Return whether src library imports any symbols from dst, assuming src is not
126+
# allowed to depend on dst.
127+
check_disallowed() {
128+
local src="$1"
129+
local dst="$2"
130+
local temp_dir="$3"
131+
local result=0
132+
133+
# Loop over symbol names exported by dst and imported by src
134+
while read symbol; do
135+
local dst_obj
136+
dst_obj=$(obj_names "$symbol" "${temp_dir}/${dst}_exports.txt")
137+
while read src_obj; do
138+
if ! check_suppress "$src_obj" "$dst_obj" "$symbol"; then
139+
echo "Error: $src_obj depends on $dst_obj symbol '$(c++filt "$symbol")', can suppess with:"
140+
echo " SUPPRESS[\"$src_obj $dst_obj $symbol\"]=1"
141+
result=1
142+
fi
143+
done < <(obj_names "$symbol" "${temp_dir}/${src}_imports.txt")
144+
done < <(comm -12 "${temp_dir}/${dst}_exported_symbols.txt" "${temp_dir}/${src}_imported_symbols.txt")
145+
return $result
146+
}
147+
148+
# Declare array to track errors which were suppressed.
149+
declare -A SUPPRESSED
150+
151+
# Return whether error should be suppressed and record suppresssion in
152+
# SUPPRESSED array.
153+
check_suppress() {
154+
local src_obj="$1"
155+
local dst_obj="$2"
156+
local symbol="$3"
157+
for suppress in "${!SUPPRESS[@]}"; do
158+
read suppress_src suppress_dst suppress_pattern <<<"$suppress"
159+
if [[ "$src_obj" == "$suppress_src" && "$dst_obj" == "$suppress_dst" && "$symbol" =~ $suppress_pattern ]]; then
160+
SUPPRESSED["$suppress"]=1
161+
return 0
162+
fi
163+
done
164+
return 1
165+
}
166+
167+
# Warn about error which were supposed to be suppress, but were not encountered.
168+
check_not_suppressed() {
169+
for suppress in "${!SUPPRESS[@]}"; do
170+
if [[ ! -v SUPPRESSED[$suppress] ]]; then
171+
echo >&2 "Warning: suppression '$suppress' was ignored, consider deleting."
172+
fi
173+
done
174+
}
175+
176+
# Check arguments.
177+
if [ "$#" = 0 ]; then
178+
BUILD_DIR="$(dirname "${BASH_SOURCE[0]}")/../../src"
179+
elif [ "$#" = 1 ]; then
180+
BUILD_DIR="$1"
181+
else
182+
echo >&2 "Error: wrong number of arguments."
183+
usage >&2
184+
exit 1
185+
fi
186+
if [ ! -f "$BUILD_DIR/Makefile" ]; then
187+
echo >&2 "Error: directory '$BUILD_DIR' does not contain a makefile, please specify path to build directory for library targets."
188+
usage >&2
189+
exit 1
190+
fi
191+
192+
# Build libraries and run checks.
193+
cd "$BUILD_DIR"
194+
# shellcheck disable=SC2046
195+
make -j"$(nproc)" $(lib_targets)
196+
TEMP_DIR="$(mktemp -d)"
197+
extract_symbols "$TEMP_DIR"
198+
if check_libraries "$TEMP_DIR"; then
199+
echo "Success! No unexpected dependencies were detected."
200+
else
201+
echo >&2 "Error: Unexpected dependencies were detected. Check previous output."
202+
fi
203+
rm -r "$TEMP_DIR"

0 commit comments

Comments
 (0)