#!/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= 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 } $1 == "validations:" { 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