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