Scripts to evaluate dependency transitive closures.

Bug: 145347092
Bug: 141258651
Bug: 69058154
Bug: 143285996

Test: run manually

Change-Id: I29d674f97ccff19ff61f675022dce052f14e0dd8
diff --git a/scripts/reverse-deps.sh b/scripts/reverse-deps.sh
new file mode 100755
index 0000000..02b7dcb
--- /dev/null
+++ b/scripts/reverse-deps.sh
@@ -0,0 +1,246 @@
+#!/bin/bash
+
+set -eu
+
+# Copyright 2020 Google Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# Tool to evaluate the transitive closure of the ninja dependency graph of the
+# files and targets depending on a given target.
+#
+# i.e. the list of things that could change after changing a target.
+
+readonly me=$(basename "${0}")
+
+readonly usage="usage: ${me} {options} target [target...]
+
+Evaluate the reverse transitive closure of ninja targets depending on one or
+more targets.
+
+Options:
+
+  -(no)quiet        Suppresses progress output to stderr and interactive
+    alias -(no)q    prompts. By default, when stderr is a tty, progress gets
+                    reported to stderr; when both stderr and stdin are tty,
+                    the script asks user whether to delete intermediate files.
+                    When suppressed or not prompted, script always deletes the
+                    temporary / intermediate files.
+  -sep=<delim>      Use 'delim' as output field separator between notice
+                    checksum and notice filename in notice output.
+                    e.g. sep='\t'
+                    (Default space)
+  -csv              Shorthand for -sep=','
+
+At minimum, before running this script, you must first run:
+$ source build/envsetup.sh
+$ lunch
+$ m nothing
+to setup the build environment, choose a target platform, and build the ninja
+dependency graph.
+"
+
+function die() { echo -e "${*}" >&2; exit 2; }
+
+# Reads one input target per line from stdin; outputs (isnotice target) tuples.
+#
+# output target is a ninja target that the input target depends on
+# isnotice in {0,1} with 1 for output targets believed to be license or notice
+#
+# only argument is the dependency depth indicator
+function getDeps() {
+    (tr '\n' '\0' | xargs -0 "${ninja_bin}" -f "${ninja_file}" -t query) \
+    | awk -v depth="${1}" '
+      BEGIN {
+        inoutput = 0
+      }
+      $0 ~ /^\S\S*:$/ {
+        inoutput = 0
+      }
+      inoutput != 0 {
+        print gensub(/^\s*/, "", "g")" "depth
+      }
+      $1 == "outputs:" {
+        inoutput = 1
+      }
+    '
+}
+
+
+if [ -z "${ANDROID_BUILD_TOP}" ]; then
+    die "${me}: Run 'lunch' to configure the build environment"
+fi
+
+if [ -z "${TARGET_PRODUCT}" ]; then
+    die "${me}: Run 'lunch' to configure the build environment"
+fi
+
+ninja_file="${ANDROID_BUILD_TOP}/out/combined-${TARGET_PRODUCT}.ninja"
+if [ ! -f "${ninja_file}" ]; then
+    die "${me}: Run 'm nothing' to build the dependency graph"
+fi
+
+ninja_bin="${ANDROID_BUILD_TOP}/prebuilts/build-tools/linux-x86/bin/ninja"
+if [ ! -x "${ninja_bin}" ]; then
+    die "${me}: Cannot find ninja executable expected at ${ninja_bin}"
+fi
+
+
+# parse the command-line
+
+declare -a targets # one or more targets to evaluate
+
+quiet=false      # whether to suppress progress
+
+sep=" "          # output separator between depth and target
+
+use_stdin=false  # whether to read targets from stdin i.e. target -
+
+while [ $# -gt 0 ]; do
+    case "${1:-}" in
+      -)
+        use_stdin=true
+      ;;
+      -*)
+        flag=$(expr "${1}" : '^-*\(.*\)$')
+        case "${flag:-}" in
+          q) ;&
+          quiet)
+            quiet=true;;
+          noq) ;&
+          noquiet)
+            quiet=false;;
+          csv)
+            sep=",";;
+          sep)
+            sep="${2?"${usage}"}"; shift;;
+          sep=*)
+            sep=$(expr "${flag}" : '^sep=\(.*\)$';;
+          *)
+            die "Unknown flag ${1}"
+          ;;
+        esac
+      ;;
+      *)
+        targets+=("${1:-}")
+      ;;
+    esac
+    shift
+done
+
+if [ ! -v targets[0] ] && ! ${use_stdin}; then
+    die "${usage}\n\nNo target specified."
+fi
+
+# showProgress when stderr is a tty
+if [ -t 2 ] && ! ${quiet}; then
+    showProgress=true
+else
+    showProgress=false
+fi
+
+# interactive when both stderr and stdin are tty
+if ${showProgress} && [ -t 0 ]; then
+    interactive=true
+else
+    interactive=false
+fi
+
+
+readonly tmpFiles=$(mktemp -d "${TMPDIR}.tdeps.XXXXXXXXX")
+if [ -z "${tmpFiles}" ]; then
+    die "${me}: unable to create temporary directory"
+fi
+
+# The deps files contain unique (isnotice target) tuples where
+# isnotice in {0,1} with 1 when ninja target `target` is a license or notice.
+readonly oldDeps="${tmpFiles}/old"
+readonly newDeps="${tmpFiles}/new"
+readonly allDeps="${tmpFiles}/all"
+
+if ${use_stdin}; then # start deps by reading 1 target per line from stdin
+  awk '
+    NF > 0 {
+      print gensub(/\s*$/, "", "g", gensub(/^\s*/, "", "g"))" "0
+    }
+  ' >"${newDeps}"
+else # start with no deps by clearing file
+  : >"${newDeps}"
+fi
+
+# extend deps by appending targets from command-line
+for idx in "${!targets[*]}"; do
+    echo "${targets[${idx}]} 0" >>"${newDeps}"
+done
+
+# remove duplicates and start with new, old and all the same
+sort -u <"${newDeps}" >"${allDeps}"
+cp "${allDeps}" "${newDeps}"
+cp "${allDeps}" "${oldDeps}"
+
+# report depth of dependenciens when showProgress
+depth=0
+
+while [ $(wc -l < "${newDeps}") -gt 0 ]; do
+    if ${showProgress}; then
+        echo "depth ${depth} has "$(wc -l < "${newDeps}")" targets" >&2
+    fi
+    depth=$(expr ${depth} + 1)
+    ( # recalculate dependencies by combining unique inputs of new deps w. old
+        cut -d\  -f1 "${newDeps}" | getDeps "${depth}"
+        cat "${oldDeps}"
+    ) | sort -n | awk '
+      BEGIN {
+        prev = ""
+      }
+      {
+        depth = $NF
+        $NF = ""
+        gsub(/\s*$/, "")
+        if ($0 != prev) {
+          print gensub(/\s*$/, "", "g")" "depth
+        }
+        prev = $0
+      }
+    ' >"${allDeps}"
+    # recalculate new dependencies as net additions to old dependencies
+    set +e
+    diff "${oldDeps}" "${allDeps}" --old-line-format='' \
+      --new-line-format='%L' --unchanged-line-format='' > "${newDeps}"
+    set -e
+    # recalculate old dependencies for next iteration
+    cp "${allDeps}" "${oldDeps}"
+done
+
+# found all deps -- clean up last iteration of old and new
+rm -f "${oldDeps}"
+rm -f "${newDeps}"
+
+if ${showProgress}; then
+    echo $(wc -l < "${allDeps}")" targets" >&2
+fi
+
+awk -v sep="${sep}" '{
+  depth = $NF
+  $NF = ""
+  gsub(/\s*$/, "")
+  print depth sep $0
+}' "${allDeps}" | sort -n
+
+if ${interactive}; then
+    echo -n "$(date '+%F %-k:%M:%S') Delete ${tmpFiles} ? [n] " >&2
+    read answer
+    case "${answer}" in [yY]*) rm -fr "${tmpFiles}";; esac
+else
+    rm -fr "${tmpFiles}"
+fi