492 lines
16 KiB
Bash
Executable File
492 lines
16 KiB
Bash
Executable File
#!/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 a given target depends on.
|
|
#
|
|
# i.e. the list of things that, if changed, could cause a change to a target.
|
|
|
|
readonly me=$(basename "${0}")
|
|
|
|
readonly usage="usage: ${me} {options} target [target...]
|
|
|
|
Evaluate the transitive closure of files and ninja targets that one or more
|
|
targets depend on.
|
|
|
|
Dependency Options:
|
|
|
|
-(no)order_deps Whether to include order-only dependencies. (Default false)
|
|
-(no)implicit Whether to include implicit dependencies. (Default true)
|
|
-(no)explicit Whether to include regular / explicit deps. (Default true)
|
|
|
|
-nofollow Unanchored regular expression. Matching paths and targets
|
|
always get reported. Their dependencies do not get reported
|
|
unless first encountered in a 'container' file type.
|
|
Multiple allowed and combined using '|'.
|
|
e.g. -nofollow='*.so' not -nofollow='.so$'
|
|
-nofollow='*.so|*.dex' or -nofollow='*.so' -nofollow='.dex'
|
|
(Defaults to no matches)
|
|
-container Unanchored regular expression. Matching file extensions get
|
|
treated as 'container' files for -nofollow option.
|
|
Multiple allowed and combines using '|'
|
|
(Default 'apex|apk|zip|jar|tar|tgz')
|
|
|
|
Output 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=','
|
|
-directories=<f> Output directory names of dependencies to 'f'.
|
|
alias -d User '/dev/stdout' to send directories to stdout. Defaults
|
|
to no directory output.
|
|
-notices=<file> Output license and notice file paths to 'file'.
|
|
alias -n Use '/dev/stdout' to send notices to stdout. Defaults to no
|
|
license/notice output.
|
|
-projects=<file> Output git project names to 'file'. Use '/dev/stdout' to
|
|
alias -p send projects to stdout. Defaults to no project output.
|
|
-targets=<fils> Output target dependencies to 'file'. Use '/dev/stdout' to
|
|
alias -t send targets to stdout.
|
|
When no directory, notice, project or target output options
|
|
given, defaults to stdout. Otherwise, defaults to no target
|
|
output.
|
|
|
|
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
|
|
function getDeps() {
|
|
(tr '\n' '\0' | xargs -0 -r "${ninja_bin}" -f "${ninja_file}" -t query) \
|
|
| awk -v include_order="${include_order_deps}" \
|
|
-v include_implicit="${include_implicit_deps}" \
|
|
-v include_explicit="${include_deps}" \
|
|
-v containers="${container_types}" \
|
|
'
|
|
BEGIN {
|
|
ininput = 0
|
|
isnotice = 0
|
|
currFileName = ""
|
|
currExt = ""
|
|
}
|
|
$1 == "outputs:" {
|
|
ininput = 0
|
|
}
|
|
ininput == 0 && $0 ~ /^\S\S*:$/ {
|
|
isnotice = ($0 ~ /.*NOTICE.*[.]txt:$/)
|
|
currFileName = gensub(/^.*[/]([^/]*)[:]$/, "\\1", "g")
|
|
currExt = gensub(/^.*[.]([^./]*)[:]$/, "\\1", "g")
|
|
}
|
|
ininput != 0 && $1 !~ /^[|][|]?/ {
|
|
if (include_explicit == "true") {
|
|
fileName = gensub(/^.*[/]([^/]*)$/, "\\1", "g")
|
|
print ( \
|
|
(isnotice && $0 !~ /^\s*build[/]soong[/]scripts[/]/) \
|
|
|| $0 ~ /NOTICE|LICEN[CS]E/ \
|
|
|| $0 ~ /(notice|licen[cs]e)[.]txt/ \
|
|
)" "(fileName == currFileName||currExt ~ "^(" containers ")$")" "gensub(/^\s*/, "", "g")
|
|
}
|
|
}
|
|
ininput != 0 && $1 == "|" {
|
|
if (include_implicit == "true") {
|
|
fileName = gensub(/^.*[/]([^/]*)$/, "\\1", "g")
|
|
$1 = ""
|
|
print ( \
|
|
(isnotice && $0 !~ /^\s*build[/]soong[/]scripts[/]/) \
|
|
|| $0 ~ /NOTICE|LICEN[CS]E/ \
|
|
|| $0 ~ /(notice|licen[cs]e)[.]txt/ \
|
|
)" "(fileName == currFileName||currExt ~ "^(" containers ")$")" "gensub(/^\s*/, "", "g")
|
|
}
|
|
}
|
|
ininput != 0 && $1 == "||" {
|
|
if (include_order == "true") {
|
|
fileName = gensub(/^.*[/]([^/]*)$/, "\\1", "g")
|
|
$1 = ""
|
|
print ( \
|
|
(isnotice && $0 !~ /^\s*build[/]soong[/]scripts[/]/) \
|
|
|| $0 ~ /NOTICE|LICEN[CS]E/ \
|
|
|| $0 ~ /(notice|licen[cs]e)[.]txt/ \
|
|
)" "(fileName == currFileName||currExt ~ "^(" containers ")$")" "gensub(/^\s*/, "", "g")
|
|
}
|
|
}
|
|
$1 == "input:" {
|
|
ininput = 1
|
|
}
|
|
'
|
|
}
|
|
|
|
# Reads one input directory per line from stdin; outputs unique git projects.
|
|
function getProjects() {
|
|
while read d; do
|
|
while [ "${d}" != '.' ] && [ "${d}" != '/' ]; do
|
|
if [ -d "${d}/.git/" ]; then
|
|
echo "${d}"
|
|
break
|
|
fi
|
|
d=$(dirname "${d}")
|
|
done
|
|
done | sort -u
|
|
}
|
|
|
|
|
|
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
|
|
|
|
readonly 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
|
|
|
|
readonly 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
|
|
|
|
include_order_deps=false # whether to trace through || "order dependencies"
|
|
include_implicit_deps=true # whether to trace through | "implicit deps"
|
|
include_deps=true # whether to trace through regular explicit deps
|
|
quiet=false # whether to suppress progress
|
|
|
|
projects_out='' # where to output the list of projects
|
|
directories_out='' # where to output the list of directories
|
|
targets_out='' # where to output the list of targets/source files
|
|
notices_out='' # where to output the list of license/notice files
|
|
|
|
sep=" " # separator between md5sum and notice filename
|
|
|
|
nofollow='' # regularexp must fully match targets to skip
|
|
|
|
container_types='' # regularexp must full match file extension
|
|
# defaults to 'apex|apk|zip|jar|tar|tgz' below.
|
|
|
|
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
|
|
order_deps)
|
|
include_order_deps=true;;
|
|
noorder_deps)
|
|
include_order_deps=false;;
|
|
implicit)
|
|
include_implicit_deps=true;;
|
|
noimplicit)
|
|
include_implicit_deps=false;;
|
|
explicit)
|
|
include_deps=true;;
|
|
noexplicit)
|
|
include_deps=false;;
|
|
csv)
|
|
sep=",";;
|
|
sep)
|
|
sep="${2?"${usage}"}"; shift;;
|
|
sep=)
|
|
sep=$(expr "${flag}" : '^sep=\(.*\)$');;
|
|
q) ;&
|
|
quiet)
|
|
quiet=true;;
|
|
noq) ;&
|
|
noquiet)
|
|
quiet=false;;
|
|
nofollow)
|
|
case "${nofollow}" in
|
|
'')
|
|
nofollow="${2?"${usage}"}";;
|
|
*)
|
|
nofollow="${nofollow}|${2?"${usage}"}";;
|
|
esac
|
|
shift
|
|
;;
|
|
nofollow=*)
|
|
case "${nofollow}" in
|
|
'')
|
|
nofollow=$(expr "${flag}" : '^nofollow=\(.*\)$');;
|
|
*)
|
|
nofollow="${nofollow}|"$(expr "${flag}" : '^nofollow=\(.*\)$');;
|
|
esac
|
|
;;
|
|
container)
|
|
container_types="${container_types}|${2?"${usage}"}";;
|
|
container=*)
|
|
container_types="${container_types}|"$(expr "${flag}" : '^container=\(.*\)$');;
|
|
p) ;&
|
|
projects)
|
|
projects_out="${2?"${usage}"}"; shift;;
|
|
p=*) ;&
|
|
projects=*)
|
|
projects_out=$(expr "${flag}" : '^.*=\(.*\)$');;
|
|
d) ;&
|
|
directores)
|
|
directories_out="${2?"${usage}"}"; shift;;
|
|
d=*) ;&
|
|
directories=*)
|
|
directories_out=$(expr "${flag}" : '^.*=\(.*\)$');;
|
|
t) ;&
|
|
targets)
|
|
targets_out="${2?"${usage}"}"; shift;;
|
|
t=*) ;&
|
|
targets=)
|
|
targets_out=$(expr "${flag}" : '^.*=\(.*\)$');;
|
|
n) ;&
|
|
notices)
|
|
notices_out="${2?"${usage}"}"; shift;;
|
|
n=*) ;&
|
|
notices=)
|
|
notices_out=$(expr "${flag}" : '^.*=\(.*\)$');;
|
|
*)
|
|
die "${usage}\n\nUnknown flag ${1}";;
|
|
esac
|
|
;;
|
|
*)
|
|
targets+=("${1:-}")
|
|
;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
|
|
# fail fast if command-line arguments are invalid
|
|
|
|
if [ ! -v targets[0] ] && ! ${use_stdin}; then
|
|
die "${usage}\n\nNo target specified."
|
|
fi
|
|
|
|
if [ -z "${projects_out}" ] \
|
|
&& [ -z "${directories_out}" ] \
|
|
&& [ -z "${targets_out}" ] \
|
|
&& [ -z "${notices_out}" ]
|
|
then
|
|
targets_out='/dev/stdout'
|
|
fi
|
|
|
|
if [ -z "${container_types}" ]; then
|
|
container_types='apex|apk|zip|jar|tar|tgz'
|
|
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 ( \
|
|
$0 ~ /NOTICE|LICEN[CS]E/ \
|
|
|| $0 ~ /(notice|licen[cs]e)[.]txt/ \
|
|
)" "gensub(/\s*$/, "", "g", gensub(/^\s*/, "", "g"))
|
|
}
|
|
' > "${newDeps}"
|
|
else # start with no deps by clearing file
|
|
: > "${newDeps}"
|
|
fi
|
|
|
|
# extend deps by appending targets from command-line
|
|
for idx in "${!targets[*]}"; do
|
|
isnotice='0'
|
|
case "${targets[${idx}]}" in
|
|
*NOTICE*) ;&
|
|
*LICEN[CS]E*) ;&
|
|
*notice.txt) ;&
|
|
*licen[cs]e.txt)
|
|
isnotice='1';;
|
|
esac
|
|
echo "${isnotice} 1 ${targets[${idx}]}" >> "${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
|
|
|
|
# 1st iteration always unfiltered
|
|
filter='cat'
|
|
while [ $(wc -l < "${newDeps}") -gt 0 ]; do
|
|
if ${showProgress}; then
|
|
echo "depth ${depth} has "$(wc -l < "${newDeps}")" targets" >&2
|
|
depth=$(expr ${depth} + 1)
|
|
fi
|
|
( # recalculate dependencies by combining unique inputs of new deps w. old
|
|
set +e
|
|
sh -c "${filter}" < "${newDeps}" | cut -d\ -f3- | getDeps
|
|
set -e
|
|
cat "${oldDeps}"
|
|
) | sort -u > "${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
|
|
# apply filters on subsequent iterations
|
|
case "${nofollow}" in
|
|
'')
|
|
filter='cat';;
|
|
*)
|
|
filter="egrep -v '^[01] 0 (${nofollow})$'"
|
|
;;
|
|
esac
|
|
# 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
|
|
|
|
if [ -n "${targets_out}" ]; then
|
|
cut -d\ -f3- "${allDeps}" | sort -u > "${targets_out}"
|
|
fi
|
|
|
|
if [ -n "${directories_out}" ] \
|
|
|| [ -n "${projects_out}" ] \
|
|
|| [ -n "${notices_out}" ]
|
|
then
|
|
readonly allDirs="${tmpFiles}/dirs"
|
|
(
|
|
cut -d\ -f3- "${allDeps}" | tr '\n' '\0' | xargs -0 dirname
|
|
) | sort -u > "${allDirs}"
|
|
if ${showProgress}; then
|
|
echo $(wc -l < "${allDirs}")" directories" >&2
|
|
fi
|
|
|
|
case "${directories_out}" in
|
|
'') : do nothing;;
|
|
*)
|
|
cat "${allDirs}" > "${directories_out}"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
if [ -n "${projects_out}" ] \
|
|
|| [ -n "${notices_out}" ]
|
|
then
|
|
readonly allProj="${tmpFiles}/projects"
|
|
set +e
|
|
egrep -v '^out[/]' "${allDirs}" | getProjects > "${allProj}"
|
|
set -e
|
|
if ${showProgress}; then
|
|
echo $(wc -l < "${allProj}")" projects" >&2
|
|
fi
|
|
|
|
case "${projects_out}" in
|
|
'') : do nothing;;
|
|
*)
|
|
cat "${allProj}" > "${projects_out}"
|
|
;;
|
|
esac
|
|
fi
|
|
|
|
case "${notices_out}" in
|
|
'') : do nothing;;
|
|
*)
|
|
readonly allNotice="${tmpFiles}/notices"
|
|
set +e
|
|
egrep '^1' "${allDeps}" | cut -d\ -f3- | egrep -v '^out/' > "${allNotice}"
|
|
set -e
|
|
cat "${allProj}" | while read proj; do
|
|
for f in LICENSE LICENCE NOTICE license.txt notice.txt; do
|
|
if [ -f "${proj}/${f}" ]; then
|
|
echo "${proj}/${f}"
|
|
fi
|
|
done
|
|
done >> "${allNotice}"
|
|
if ${showProgress}; then
|
|
echo $(cat "${allNotice}" | sort -u | wc -l)" notice targets" >&2
|
|
fi
|
|
readonly hashedNotice="${tmpFiles}/hashednotices"
|
|
( # md5sum outputs checksum space indicator(space or *) filename newline
|
|
set +e
|
|
sort -u "${allNotice}" | tr '\n' '\0' | xargs -0 -r md5sum 2>/dev/null
|
|
set -e
|
|
# use sed to replace space and indicator with separator
|
|
) > "${hashedNotice}"
|
|
if ${showProgress}; then
|
|
echo $(cut -d\ -f2- "${hashedNotice}" | sort -u | wc -l)" notice files" >&2
|
|
echo $(cut -d\ -f1 "${hashedNotice}" | sort -u | wc -l)" distinct notices" >&2
|
|
fi
|
|
sed 's/^\([^ ]*\) [* ]/\1'"${sep}"'/g' "${hashedNotice}" | sort > "${notices_out}"
|
|
;;
|
|
esac
|
|
|
|
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
|