xref: /aosp_15_r20/build/soong/scripts/reverse-deps.sh (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1#!/bin/bash
2
3set -eu
4
5# Copyright 2020 Google Inc. All rights reserved.
6#
7# Licensed under the Apache License, Version 2.0 (the "License");
8# you may not use this file except in compliance with the License.
9# You may obtain a copy of the License at
10#
11#     http://www.apache.org/licenses/LICENSE-2.0
12#
13# Unless required by applicable law or agreed to in writing, software
14# distributed under the License is distributed on an "AS IS" BASIS,
15# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16# See the License for the specific language governing permissions and
17# limitations under the License.
18
19# Tool to evaluate the transitive closure of the ninja dependency graph of the
20# files and targets depending on a given target.
21#
22# i.e. the list of things that could change after changing a target.
23
24readonly me=$(basename "${0}")
25
26readonly usage="usage: ${me} {options} target [target...]
27
28Evaluate the reverse transitive closure of ninja targets depending on one or
29more targets.
30
31Options:
32
33  -(no)quiet        Suppresses progress output to stderr and interactive
34    alias -(no)q    prompts. By default, when stderr is a tty, progress gets
35                    reported to stderr; when both stderr and stdin are tty,
36                    the script asks user whether to delete intermediate files.
37                    When suppressed or not prompted, script always deletes the
38                    temporary / intermediate files.
39  -sep=<delim>      Use 'delim' as output field separator between notice
40                    checksum and notice filename in notice output.
41                    e.g. sep='\t'
42                    (Default space)
43  -csv              Shorthand for -sep=','
44
45At minimum, before running this script, you must first run:
46$ source build/envsetup.sh
47$ lunch
48$ m nothing
49to setup the build environment, choose a target platform, and build the ninja
50dependency graph.
51"
52
53function die() { echo -e "${*}" >&2; exit 2; }
54
55# Reads one input target per line from stdin; outputs (isnotice target) tuples.
56#
57# output target is a ninja target that the input target depends on
58# isnotice in {0,1} with 1 for output targets believed to be license or notice
59#
60# only argument is the dependency depth indicator
61function getDeps() {
62    (tr '\n' '\0' | xargs -0 "${ninja_bin}" -f "${ninja_file}" -t query) \
63    | awk -v depth="${1}" '
64      BEGIN {
65        inoutput = 0
66      }
67      $0 ~ /^\S\S*:$/ {
68        inoutput = 0
69      }
70      $1 == "validations:" {
71        inoutput = 0
72      }
73      inoutput != 0 {
74        print gensub(/^\s*/, "", "g")" "depth
75      }
76      $1 == "outputs:" {
77        inoutput = 1
78      }
79    '
80}
81
82
83if [ -z "${ANDROID_BUILD_TOP}" ]; then
84    die "${me}: Run 'lunch' to configure the build environment"
85fi
86
87if [ -z "${TARGET_PRODUCT}" ]; then
88    die "${me}: Run 'lunch' to configure the build environment"
89fi
90
91ninja_file="${ANDROID_BUILD_TOP}/out/combined-${TARGET_PRODUCT}.ninja"
92if [ ! -f "${ninja_file}" ]; then
93    die "${me}: Run 'm nothing' to build the dependency graph"
94fi
95
96ninja_bin="${ANDROID_BUILD_TOP}/prebuilts/build-tools/linux-x86/bin/ninja"
97if [ ! -x "${ninja_bin}" ]; then
98    die "${me}: Cannot find ninja executable expected at ${ninja_bin}"
99fi
100
101
102# parse the command-line
103
104declare -a targets # one or more targets to evaluate
105
106quiet=false      # whether to suppress progress
107
108sep=" "          # output separator between depth and target
109
110use_stdin=false  # whether to read targets from stdin i.e. target -
111
112while [ $# -gt 0 ]; do
113    case "${1:-}" in
114      -)
115        use_stdin=true
116      ;;
117      -*)
118        flag=$(expr "${1}" : '^-*\(.*\)$')
119        case "${flag:-}" in
120          q) ;&
121          quiet)
122            quiet=true;;
123          noq) ;&
124          noquiet)
125            quiet=false;;
126          csv)
127            sep=",";;
128          sep)
129            sep="${2?"${usage}"}"; shift;;
130          sep=*)
131            sep=$(expr "${flag}" : '^sep=\(.*\)$';;
132          *)
133            die "Unknown flag ${1}"
134          ;;
135        esac
136      ;;
137      *)
138        targets+=("${1:-}")
139      ;;
140    esac
141    shift
142done
143
144if [ ! -v targets[0] ] && ! ${use_stdin}; then
145    die "${usage}\n\nNo target specified."
146fi
147
148# showProgress when stderr is a tty
149if [ -t 2 ] && ! ${quiet}; then
150    showProgress=true
151else
152    showProgress=false
153fi
154
155# interactive when both stderr and stdin are tty
156if ${showProgress} && [ -t 0 ]; then
157    interactive=true
158else
159    interactive=false
160fi
161
162
163readonly tmpFiles=$(mktemp -d "${TMPDIR}.tdeps.XXXXXXXXX")
164if [ -z "${tmpFiles}" ]; then
165    die "${me}: unable to create temporary directory"
166fi
167
168# The deps files contain unique (isnotice target) tuples where
169# isnotice in {0,1} with 1 when ninja target `target` is a license or notice.
170readonly oldDeps="${tmpFiles}/old"
171readonly newDeps="${tmpFiles}/new"
172readonly allDeps="${tmpFiles}/all"
173
174if ${use_stdin}; then # start deps by reading 1 target per line from stdin
175  awk '
176    NF > 0 {
177      print gensub(/\s*$/, "", "g", gensub(/^\s*/, "", "g"))" "0
178    }
179  ' >"${newDeps}"
180else # start with no deps by clearing file
181  : >"${newDeps}"
182fi
183
184# extend deps by appending targets from command-line
185for idx in "${!targets[*]}"; do
186    echo "${targets[${idx}]} 0" >>"${newDeps}"
187done
188
189# remove duplicates and start with new, old and all the same
190sort -u <"${newDeps}" >"${allDeps}"
191cp "${allDeps}" "${newDeps}"
192cp "${allDeps}" "${oldDeps}"
193
194# report depth of dependenciens when showProgress
195depth=0
196
197while [ $(wc -l < "${newDeps}") -gt 0 ]; do
198    if ${showProgress}; then
199        echo "depth ${depth} has "$(wc -l < "${newDeps}")" targets" >&2
200    fi
201    depth=$(expr ${depth} + 1)
202    ( # recalculate dependencies by combining unique inputs of new deps w. old
203        cut -d\  -f1 "${newDeps}" | getDeps "${depth}"
204        cat "${oldDeps}"
205    ) | sort -n | awk '
206      BEGIN {
207        prev = ""
208      }
209      {
210        depth = $NF
211        $NF = ""
212        gsub(/\s*$/, "")
213        if ($0 != prev) {
214          print gensub(/\s*$/, "", "g")" "depth
215        }
216        prev = $0
217      }
218    ' >"${allDeps}"
219    # recalculate new dependencies as net additions to old dependencies
220    set +e
221    diff "${oldDeps}" "${allDeps}" --old-line-format='' \
222      --new-line-format='%L' --unchanged-line-format='' > "${newDeps}"
223    set -e
224    # recalculate old dependencies for next iteration
225    cp "${allDeps}" "${oldDeps}"
226done
227
228# found all deps -- clean up last iteration of old and new
229rm -f "${oldDeps}"
230rm -f "${newDeps}"
231
232if ${showProgress}; then
233    echo $(wc -l < "${allDeps}")" targets" >&2
234fi
235
236awk -v sep="${sep}" '{
237  depth = $NF
238  $NF = ""
239  gsub(/\s*$/, "")
240  print depth sep $0
241}' "${allDeps}" | sort -n
242
243if ${interactive}; then
244    echo -n "$(date '+%F %-k:%M:%S') Delete ${tmpFiles} ? [n] " >&2
245    read answer
246    case "${answer}" in [yY]*) rm -fr "${tmpFiles}";; esac
247else
248    rm -fr "${tmpFiles}"
249fi
250