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