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]="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