blob: 32adfc4a8ea0b69c620fb06da812b9fce473daf4 [file] [log] [blame]
#!/bin/bash
#
# Copyright (C) 2020 The Android Open Source Project
#
# 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.
#
set -e
function usage() {
echo 'NAME'
echo ' simplify-build-failure.sh'
echo
echo 'SYNOPSIS'
echo " $0 (--task <gradle task> <error message> | --command <shell command> ) [--continue] [--limit-to-path <file path>] [--check-lines-in <subfile path>] [--num-jobs <count>]"
echo
echo DESCRIPTION
echo ' Searches for a minimal set of files and/or lines required to reproduce a given build failure'
echo
echo OPTIONS
echo
echo ' --task <gradle task> <error message>`'
echo ' Specifies that `./gradlew <gradle task>` must fail with error message <error message>'
echo
echo ' --command <shell command>'
echo ' Specifies that <shell command> must succeed.'
echo
echo ' --continue'
echo ' Attempts to pick up from a previous invocation of simplify-build-failure.sh'
echo
echo ' --limit-to-path <limitPath>'
echo ' Will check only <limitPath> (plus subdirectories, if present) for possible simplications. This can make the simplification process faster if there are paths that you know are'
echo ' uninteresting to you'
echo
echo ' --check-lines-in <subfile path>'
echo ' Specifies that individual lines in files in <subfile path> will be considered for removal, too'
echo ' --num-jobs <count>'
echo ' Specifies the number of jobs to run at once'
exit 1
}
function notify() {
echo simplify-build-failure.sh $1
notify-send simplify-build-failure.sh $1
}
function failed() {
notify failed
exit 1
}
gradleTasks=""
errorMessage=""
gradleCommand=""
grepCommand=""
testCommand=""
resume=false
subfilePath=""
limitToPath=""
numJobs="1"
export ALLOW_MISSING_PROJECTS=true # so that if we delete entire projects then the AndroidX build doesn't think we made a spelling mistake
workingDir="$(pwd)"
cd "$(dirname $0)"
scriptPath="$(pwd)"
cd ../..
supportRoot="$(pwd)"
checkoutRoot="$(cd $supportRoot/../.. && pwd)"
tempDir="$checkoutRoot/simplify-tmp"
# If the this script was run from a subdirectory, then we run our test command from the same subdirectory
commandSubdir="$(echo $workingDir | sed "s|^$supportRoot|.|g")"
if [ ! -e "$workingDir/gradlew" ]; then
echo "Error; ./gradlew does not exist. Must cd to a dir containing a ./gradlew first"
# so that this script knows which gradlew to use (in frameworks/support or frameworks/support/ui)
exit 1
fi
while [ "$1" != "" ]; do
arg="$1"
shift
if [ "$arg" == "--continue" ]; then
resume=true
continue
fi
if [ "$arg" == "--task" ]; then
gradleTasks="$1"
shift
errorMessage="$1"
shift
if [ "$gradleTasks" == "" ]; then
usage
fi
if [ "$errorMessage" == "" ]; then
usage
fi
gradleCommand="OUT_DIR=out ./gradlew $gradleTasks > log 2>&1"
grepCommand="grep \"$errorMessage\" log"
# Sleep in case Gradle fails very quickly
# We don't want to run too many Gradle commands in a row or else the daemons might get confused
testCommand="cd $commandSubdir && $gradleCommand; sleep 2; $grepCommand"
continue
fi
if [ "$arg" == "--command" ]; then
if [ "$1" == "" ]; then
usage
fi
testCommand="cd $commandSubdir && $1"
shift
gradleCommand=""
grepCommand=""
if echo "$testCommand" | grep -v OUT_DIR 2>/dev/null; then
echo "Error: must set OUT_DIR in the test command to prevent concurrent Gradle executions from interfering with each other"
exit 1
fi
continue
fi
if [ "$arg" == "--check-lines-in" ]; then
subfilePath="$1"
shift
continue
fi
if [ "$arg" == "--limit-to-path" ]; then
limitToPath="$1"
shift
continue
fi
if [ "$arg" == "--num-jobs" ]; then
numJobs="$1"
shift
continue
fi
echo "Unrecognized argument '$arg'"
usage
done
if [ "$testCommand" == "" ]; then
usage
fi
if [ "$resume" == "true" ]; then
if [ -d "$tempDir" ]; then
echo "Not deleting temp dir $tempDir"
fi
else
echo "Removing temp dir $tempDir"
rm "$tempDir" -rf
fi
referencePassingDir="$tempDir/base"
referenceFailingDir="$tempDir/failing"
rm "$referencePassingDir" -rf
if [ "$limitToPath" != "" ]; then
mkdir -p "$(dirname $referencePassingDir)"
cp -r "$supportRoot" "$referencePassingDir"
rm "$referencePassingDir/$limitToPath" -rf
else
mkdir -p "$referencePassingDir"
fi
if [ "$subfilePath" != "" ]; then
if [ ! -e "$subfilePath" ]; then
echo "$subfilePath" does not exist
exit 1
fi
fi
filtererStep1Work="$tempDir"
filtererStep1Output="$filtererStep1Work/bestResults"
fewestFilesOutputPath="$tempDir/fewestFiles"
if echo "$resume" | grep "true" >/dev/null && stat "$fewestFilesOutputPath" >/dev/null 2>/dev/null; then
echo "Skipping first execution of diff-filterer, $fewestFilesOutputPath already exists"
else
if [ "$resume" == "true" ]; then
if stat "$filtererStep1Output" >/dev/null 2>/dev/null; then
echo "Reusing $filtererStep1Output to resume first execution of diff-filterer"
# Copy the previous results to resume from
rm "$referenceFailingDir" -rf
cp -rT "$filtererStep1Output" "$referenceFailingDir"
else
echo "Cannot resume previous execution; neither $fewestFilesOutputPath nor $filtererStep1Output exists"
exit 1
fi
else
# make a backup of the code so that the user can still make modifications to the source tree without interfering with diff-filterer.py
rm "$referenceFailingDir" -rf
cp -rT . "$referenceFailingDir"
# remove some unhelpful settings
sed -i 's/.*Werror.*//' "$referenceFailingDir/buildSrc/build.gradle"
fi
echo Running diff-filterer.py once to identify the minimal set of files needed to reproduce the error
if ./development/file-utils/diff-filterer.py --assume-no-side-effects --work-path $filtererStep1Work --num-jobs "$numJobs" "$referenceFailingDir" "$referencePassingDir" "$testCommand"; then
echo diff-filterer completed successfully
else
failed
fi
fi
if [ "$subfilePath" == "" ]; then
echo Splitting files into individual lines was not enabled. Done. See results at $filtererStep1Work/bestResults
else
if [ "$subfilePath" == "." ]; then
subfilePath=""
fi
if echo "$resume" | grep true >/dev/null && stat $fewestFilesOutputPath >/dev/null 2>/dev/null; then
echo "Skipping recopying $filtererStep1Output to $fewestFilesOutputPath"
else
echo Copying minimal set of files into $fewestFilesOutputPath
rm -rf "$fewestFilesOutputPath"
cp -rT "$filtererStep1Output" "$fewestFilesOutputPath"
fi
echo Creating working directory for identifying individually smallest files
noFunctionBodies_Passing="$tempDir/noFunctionBodies_Passing"
noFunctionBodies_goal="$tempDir/noFunctionBodies_goal"
noFunctionBodies_work="work"
noFunctionBodies_sandbox="$noFunctionBodies_work/$subfilePath"
noFunctionBodies_output="$tempDir/noFunctionBodies_output"
# set up command for running diff-filterer against diffs within files
filtererOptions="--num-jobs $numJobs"
if echo $subfilePath | grep -v buildSrc >/dev/null 2>/dev/null; then
# If we're not making changes in buildSrc, then we want to keep the gradle caches around for more speed
# If we are making changes in buildSrc, then Gradle doesn't necessarily do up-to-date checks correctly, and we want to clear the caches between builds
filtererOptions="$filtererOptions --assume-no-side-effects"
else
if [ "$grepCommand" != "" ]; then
filtererOptions="$filtererOptions --assume-no-side-effects"
# If we're making changes in buildSrc, then we want to make sure that a clean build passes because Gradle doesn't always do up-to-date checks correctly when we're making strange changes in buildSrc
# However, the build runs much more quickly when incremental than when clean
# So, we first run an incremental build and then if it passes we run a clean build
testCommand="cd $commandSubdir && $gradleCommand; $grepCommand && rm log out -rf && $gradleCommand --no-daemon; $grepCommand"
fi
fi
if echo "$resume" | grep true >/dev/null && stat "$noFunctionBodies_output" >/dev/null 2>/dev/null; then
echo "Skipping asking diff-filterer to remove function bodies because $noFunctionBodies_output already exists"
else
echo Splitting files into smaller pieces
rm -rf "$noFunctionBodies_Passing" "$noFunctionBodies_goal"
mkdir -p "$noFunctionBodies_Passing" "$noFunctionBodies_goal"
cd "$noFunctionBodies_Passing"
cp -rT "$fewestFilesOutputPath" "$noFunctionBodies_work"
cp -rT "$noFunctionBodies_Passing" "$noFunctionBodies_goal"
splitsPath="${subfilePath}.split"
"${scriptPath}/impl/split.sh" --consolidate-leaves "$noFunctionBodies_sandbox" "$splitsPath"
rm "$noFunctionBodies_sandbox" -rf
echo Removing deepest lines
cd "$noFunctionBodies_goal"
"${scriptPath}/impl/split.sh" --remove-leaves "$noFunctionBodies_sandbox" "$splitsPath"
rm "$noFunctionBodies_sandbox" -rf
# TODO: maybe we should make diff-filterer.py directly support checking individual line differences within files rather than first running split.sh and asking diff-filterer.py to run join.sh
# It would be harder to implement in diff-filterer.py though because diff-filterer.py would also need to support comparing against nonempty files too
echo Running diff-filterer.py again to identify which function bodies can be removed
if "$supportRoot/development/file-utils/diff-filterer.py" --assume-input-states-are-correct $filtererOptions --work-path "$(cd $supportRoot/../.. && pwd)" "$noFunctionBodies_Passing" "$noFunctionBodies_goal" "${scriptPath}/impl/join.sh ${splitsPath} ${noFunctionBodies_sandbox} && cd ${noFunctionBodies_work} && $testCommand"; then
echo diff-filterer completed successfully
else
failed
fi
echo Re-joining the files
rm -rf "${noFunctionBodies_output}"
cp -rT "$(cd $supportRoot/../../bestResults && pwd)" "${noFunctionBodies_output}"
cd "${noFunctionBodies_output}"
"${scriptPath}/impl/join.sh" "${splitsPath}" "${noFunctionBodies_sandbox}"
fi
# prepare for another invocation of diff-filterer, to remove other code that is now unused
smallestFilesInput="$tempDir/smallestFilesInput"
smallestFilesGoal="$tempDir/smallestFilesGoal"
smallestFilesWork="work"
smallestFilesSandbox="$smallestFilesWork/$subfilePath"
rm -rf "$smallestFilesInput" "$smallestFilesGoal"
mkdir -p "$smallestFilesInput"
cp -rT "${noFunctionBodies_output}" "$smallestFilesInput"
echo Splitting files into individual lines
cd "$smallestFilesInput"
splitsPath="${subfilePath}.split"
"${scriptPath}/impl/split.sh" "$smallestFilesSandbox" "$splitsPath"
rm "$smallestFilesSandbox" -rf
# Make a dir holding the destination file state
if [ "$limitToPath" != "" ]; then
# The user said they were only interested in trying to delete files under a certain path
# So, our target state is the original state minus that path (and its descendants)
mkdir -p "$smallestFilesGoal"
cp -rT "$smallestFilesInput/$smallestFilesWork" "$smallestFilesGoal/$smallestFilesWork"
cd "$smallestFilesGoal/$smallestFilesWork"
rm "$limitToPath" -rf
cd -
else
# The user didn't request to limit the search to a specific path, so we try to delete as many
# files as possible
mkdir -p "$smallestFilesGoal"
fi
echo Running diff-filterer.py again to identify the minimal set of lines needed to reproduce the error
if "$supportRoot/development/file-utils/diff-filterer.py" $filtererOptions --work-path "$(cd $supportRoot/../.. && pwd)" "$smallestFilesInput" "$smallestFilesGoal" "${scriptPath}/impl/join.sh ${splitsPath} ${smallestFilesSandbox} && cd ${smallestFilesWork} && $testCommand"; then
echo diff-filterer completed successfully
else
failed
fi
echo Re-joining the files
smallestFilesOutput="$tempDir/smallestFilesOutput"
rm -rf "$smallestFilesOutput"
cp -rT "$(cd $supportRoot/../../bestResults && pwd)" "${smallestFilesOutput}"
cd "${smallestFilesOutput}"
"${scriptPath}/impl/join.sh" "${splitsPath}" "${smallestFilesSandbox}"
echo "Done. See simplest discovered reproduction test case at ${smallestFilesOutput}"
fi
notify succeeded