aboutsummaryrefslogtreecommitdiff
path: root/contrib/devtools/check-deps.sh
blob: 2fce249945ba13a7b4750e699927cd2545624487 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env bash

export LC_ALL=C
set -Eeuo pipefail

# Declare paths to libraries
declare -A LIBS
LIBS[cli]="libbitcoin_cli.a"
LIBS[common]="libbitcoin_common.a"
LIBS[consensus]="libbitcoin_consensus.a"
LIBS[crypto]="crypto/libbitcoin_crypto.a crypto/libbitcoin_crypto_x86_shani.a crypto/libbitcoin_crypto_sse41.a crypto/libbitcoin_crypto_avx2.a"
LIBS[node]="libbitcoin_node.a"
LIBS[util]="util/libbitcoin_util.a"
LIBS[wallet]="wallet/libbitcoin_wallet.a"

# Declare allowed dependencies "X Y" where X is allowed to depend on Y. This
# list is taken from doc/design/libraries.md.
ALLOWED_DEPENDENCIES=(
    "cli common"
    "cli util"
    "common consensus"
    "common crypto"
    "common util"
    "consensus crypto"
    "node common"
    "node consensus"
    "node crypto"
    "node kernel"
    "node util"
    "util crypto"
    "wallet common"
    "wallet crypto"
    "wallet util"
)

# Add minor dependencies omitted from doc/design/libraries.md to keep the
# dependency diagram simple.
ALLOWED_DEPENDENCIES+=(
    "wallet consensus"
)

# Declare list of known errors that should be suppressed.
declare -A SUPPRESS
# init.cpp file currently calls Berkeley DB sanity check function on startup, so
# there is an undocumented dependency of the node library on the wallet library.
SUPPRESS["init.cpp.o bdb.cpp.o _ZN6wallet27BerkeleyDatabaseSanityCheckEv"]=1
# init/common.cpp file calls InitError and InitWarning from interface_ui which
# is currently part of the node library. interface_ui should just be part of the
# common library instead, and is moved in
# https://github.com/bitcoin/bitcoin/issues/10102
SUPPRESS["common.cpp.o interface_ui.cpp.o _Z11InitWarningRK13bilingual_str"]=1
SUPPRESS["common.cpp.o interface_ui.cpp.o _Z9InitErrorRK13bilingual_str"]=1
# rpc/external_signer.cpp adds defines node RPC methods but is built as part of the
# common library. It should be moved to the node library instead.
SUPPRESS["external_signer.cpp.o server.cpp.o _ZN9CRPCTable13appendCommandERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEPK11CRPCCommand"]=1

usage() {
   echo "Usage: $(basename "${BASH_SOURCE[0]}") [BUILD_DIR]"
}

# Output makefile targets, converting library .a paths to CMake targets
lib_targets() {
  for lib in "${!LIBS[@]}"; do
      for lib_path in ${LIBS[$lib]}; do
          local name="${lib_path##*/}"
          name="${name#lib}"
          name="${name%.a}"
          echo "$name"
      done
  done
}

# Extract symbol names and object names and write to text files
extract_symbols() {
    local temp_dir="$1"
    for lib in "${!LIBS[@]}"; do
        for lib_path in ${LIBS[$lib]}; do
            nm -o "$lib_path" | { grep ' T \| W ' || true; } | awk '{print $3, $1}' >> "${temp_dir}/${lib}_exports.txt"
            nm -o "$lib_path" | { grep ' U ' || true; } | awk '{print $3, $1}' >> "${temp_dir}/${lib}_imports.txt"
            awk '{print $1}' "${temp_dir}/${lib}_exports.txt" | sort -u > "${temp_dir}/${lib}_exported_symbols.txt"
            awk '{print $1}' "${temp_dir}/${lib}_imports.txt" | sort -u > "${temp_dir}/${lib}_imported_symbols.txt"
        done
    done
}

# Lookup object name(s) corresponding to symbol name in text file
obj_names() {
    local symbol="$1"
    local txt_file="$2"
    sed -n "s/^$symbol [^:]\\+:\\([^:]\\+\\):[^:]*\$/\\1/p" "$txt_file" | sort -u
}

# Iterate through libraries and find disallowed dependencies
check_libraries() {
    local temp_dir="$1"
    local result=0
    for src in "${!LIBS[@]}"; do
        for dst in "${!LIBS[@]}"; do
            if [ "$src" != "$dst" ] && ! is_allowed "$src" "$dst"; then
                if ! check_disallowed "$src" "$dst" "$temp_dir"; then
                    result=1
                fi
            fi
        done
    done
    check_not_suppressed
    return $result
}

# Return whether src library is allowed to depend on dst.
is_allowed() {
    local src="$1"
    local dst="$2"
    for allowed in "${ALLOWED_DEPENDENCIES[@]}"; do
        if [ "$src $dst" = "$allowed" ]; then
            return 0
        fi
    done
    return 1
}

# Return whether src library imports any symbols from dst, assuming src is not
# allowed to depend on dst.
check_disallowed() {
    local src="$1"
    local dst="$2"
    local temp_dir="$3"
    local result=0

    # Loop over symbol names exported by dst and imported by src
    while read symbol; do
        local dst_obj
        dst_obj=$(obj_names "$symbol" "${temp_dir}/${dst}_exports.txt")
        while read src_obj; do
            if ! check_suppress "$src_obj" "$dst_obj" "$symbol"; then
                echo "Error: $src_obj depends on $dst_obj symbol '$(c++filt "$symbol")', can suppess with:"
                echo "    SUPPRESS[\"$src_obj $dst_obj $symbol\"]=1"
                result=1
            fi
        done < <(obj_names "$symbol" "${temp_dir}/${src}_imports.txt")
    done < <(comm -12 "${temp_dir}/${dst}_exported_symbols.txt" "${temp_dir}/${src}_imported_symbols.txt")
    return $result
}

# Declare array to track errors which were suppressed.
declare -A SUPPRESSED

# Return whether error should be suppressed and record suppresssion in
# SUPPRESSED array.
check_suppress() {
    local src_obj="$1"
    local dst_obj="$2"
    local symbol="$3"
    for suppress in "${!SUPPRESS[@]}"; do
        read suppress_src suppress_dst suppress_pattern <<<"$suppress"
        if [[ "$src_obj" == "$suppress_src" && "$dst_obj" == "$suppress_dst" && "$symbol" =~ $suppress_pattern ]]; then
            SUPPRESSED["$suppress"]=1
            return 0
        fi
    done
    return 1
}

# Warn about error which were supposed to be suppress, but were not encountered.
check_not_suppressed() {
    for suppress in "${!SUPPRESS[@]}"; do
        if [[ ! -v SUPPRESSED[$suppress] ]]; then
            echo >&2 "Warning: suppression '$suppress' was ignored, consider deleting."
        fi
    done
}

# Check arguments.
if [ "$#" = 0 ]; then
    BUILD_DIR="$(dirname "${BASH_SOURCE[0]}")/../../build"
elif [ "$#" = 1 ]; then
    BUILD_DIR="$1"
else
    echo >&2 "Error: wrong number of arguments."
    usage >&2
    exit 1
fi
if [ ! -f "$BUILD_DIR/Makefile" ]; then
    echo >&2 "Error: directory '$BUILD_DIR' does not contain a makefile, please specify path to build directory for library targets."
    usage >&2
    exit 1
fi

# Build libraries and run checks.
# shellcheck disable=SC2046
cmake --build "$BUILD_DIR" -j"$(nproc)" -t $(lib_targets)
TEMP_DIR="$(mktemp -d)"
cd "$BUILD_DIR/src"
extract_symbols "$TEMP_DIR"
if check_libraries "$TEMP_DIR"; then
    echo "Success! No unexpected dependencies were detected."
    RET=0
else
    echo >&2 "Error: Unexpected dependencies were detected. Check previous output."
    RET=1
fi
rm -r "$TEMP_DIR"
exit $RET