check-deps.sh raw

   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