1#!/bin/bash 2# 3# Copyright (C) 2020 The Android Open Source Project 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14# See the License for the specific language governing permissions and 15# limitations under the License. 16# 17 18set -e 19 20function usage() { 21 echo 'NAME' 22 echo ' simplify-build-failure.sh' 23 echo 24 echo 'SYNOPSIS' 25 echo " $0 (--task <gradle task> <other gradle arguments> <error message> [--clean] | --command <shell command> ) [--continue] [--limit-to-path <file path>] [--check-lines-in <subfile path>] [--num-jobs <count>]" 26 echo 27 echo DESCRIPTION 28 echo ' Searches for a minimal set of files and/or lines required to reproduce a given build failure' 29 echo 30 echo OPTIONS 31 echo 32 echo ' --task <gradle task> <other gradle arguments> <error message>`' 33 echo ' Specifies that `./gradlew <gradle task>` must fail with error message <error message>' 34 echo 35 echo ' --command <shell command>' 36 echo ' Specifies that <shell command> must succeed.' 37 echo 38 echo ' --continue' 39 echo ' Attempts to pick up from a previous invocation of simplify-build-failure.sh' 40 echo 41 echo ' --limit-to-path <limitPath>' 42 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' 43 echo ' uninteresting to you' 44 echo 45 echo ' --check-lines-in <subfile path>' 46 echo ' Specifies that individual lines in files in <subfile path> will be considered for removal, too' 47 echo 48 echo ' --num-jobs <count>' 49 echo ' Specifies the number of jobs to run at once' 50 echo 51 echo ' --clean' 52 echo ' Specifies that each build should start from a consistent state' 53 exit 1 54} 55 56function notify() { 57 echo simplify-build-failure.sh $1 58 notify-send simplify-build-failure.sh $1 59} 60 61function failed() { 62 notify failed 63 exit 1 64} 65 66gradleTasks="" 67gradleExtraArguments="" 68errorMessage="" 69gradleCommand="" 70grepCommand="" 71testCommand="" 72resume=false 73subfilePath="" 74limitToPath="" 75numJobs="auto" 76clean="false" 77 78export ALLOW_MISSING_PROJECTS=true # so that if we delete entire projects then the AndroidX build doesn't think we made a spelling mistake 79 80workingDir="$(pwd)" 81cd "$(dirname $0)" 82scriptPath="$(pwd)" 83cd ../.. 84supportRoot="$(pwd)" 85checkoutRoot="$(cd $supportRoot/../.. && pwd)" 86tempDir="$checkoutRoot/simplify-tmp" 87 88pathsToNotShrink="gradlew" 89 90if [ ! -e "$workingDir/gradlew" ]; then 91 echo "Error; ./gradlew does not exist. Must cd to a dir containing a ./gradlew first" 92 # so that this script knows which gradlew to use (in frameworks/support or frameworks/support/ui) 93 exit 1 94fi 95 96while [ "$1" != "" ]; do 97 arg="$1" 98 shift 99 if [ "$arg" == "--continue" ]; then 100 resume=true 101 continue 102 fi 103 if [ "$arg" == "--task" ]; then 104 gradleTasks="$1" 105 if [ "$gradleTasks" == "" ]; then 106 usage 107 fi 108 shift 109 gradleExtraArguments="$1" 110 shift 111 errorMessage="$1" 112 if [ "$errorMessage" == "" ]; then 113 usage 114 fi 115 shift 116 117 gradleCommand="OUT_DIR=out ./gradlew $gradleExtraArguments >log 2>&1" 118 grepCommand="$scriptPath/impl/grepOrTail.sh \"$errorMessage\" log" 119 continue 120 fi 121 if [ "$arg" == "--command" ]; then 122 if [ "$1" == "" ]; then 123 usage 124 fi 125 testCommand="$1" 126 shift 127 gradleCommand="" 128 grepCommand="" 129 if echo "$testCommand" | grep -v OUT_DIR 2>/dev/null; then 130 testCommand="export OUT_DIR=out; $testCommand" 131 else 132 echo "Sorry, customizing OUT_DIR is not supported at the moment because we want impl/join.sh to be able to detect it and skip deleting it" 133 exit 1 134 fi 135 continue 136 fi 137 if [ "$arg" == "--check-lines-in" ]; then 138 subfilePath="$1" 139 # normalize path 140 subfilePath="$(realpath $subfilePath --relative-to=.)" 141 shift 142 continue 143 fi 144 if [ "$arg" == "--limit-to-path" ]; then 145 limitToPath="$1" 146 shift 147 continue 148 fi 149 if [ "$arg" == "--num-jobs" ]; then 150 numJobs="$1" 151 shift 152 continue 153 fi 154 if [ "$arg" == "--clean" ]; then 155 clean=true 156 continue 157 fi 158 echo "Unrecognized argument '$arg'" 159 usage 160done 161 162if [ "$gradleCommand" == "" ]; then 163 if [ "$clean" == "true" ]; then 164 echo "Option --clean requires option --task" 165 usage 166 fi 167 if [ "$testCommand" == "" ]; then 168 usage 169 fi 170fi 171 172# delete temp dir if not resuming 173if [ "$resume" == "true" ]; then 174 if [ -d "$tempDir" ]; then 175 echo "Not deleting temp dir $tempDir" 176 fi 177else 178 echo "Removing temp dir $tempDir" 179 rm "$tempDir" -rf 180fi 181 182referencePassingDir="$tempDir/base" 183referenceFailingDir="$tempDir/failing" 184# backup code so user can keep editing 185if [ ! -e "$referenceFailingDir" ]; then 186 echo backing up frameworks/support into "$referenceFailingDir" in case you want to continue to make modifications or run other builds 187 rm "$referenceFailingDir" -rf 188 mkdir -p "$tempDir" 189 cp -rT . "$referenceFailingDir" 190 # remove some unhelpful settings 191 sed -i 's/.*Werror.*//' "$referenceFailingDir/buildSrc/shared.gradle" 192 sed -i 's/.*Exception.*cannot include.*//' "$referenceFailingDir/settings.gradle" 193 # remove some generated files that we don't want diff-filterer.py to track 194 rm -rf "$referenceFailingDir/.gradle" "$referenceFailingDir/buildSrc/.gradle" "$referenceFailingDir/out" 195 rm -rf "$referenceFailingDir/tasks" # generated by simplify-build-failure and could be inadvertently copied into source by the user 196fi 197 198# compute destination state, which is usually almost empty 199rm "$referencePassingDir" -rf 200if [ "$limitToPath" != "" ]; then 201 mkdir -p "$(dirname $referencePassingDir)" 202 cp -r "$supportRoot" "$referencePassingDir" 203 rm "$referencePassingDir/$limitToPath" -rf 204else 205 mkdir -p "$referencePassingDir" 206 # restore any special files that we don't want to shrink 207 for path in $pathsToNotShrink; do 208 cp "$supportRoot/$path" "$referencePassingDir" 209 done 210fi 211 212if [ "$subfilePath" != "" ]; then 213 if [ ! -e "$subfilePath" ]; then 214 echo "$subfilePath" does not exist 215 exit 1 216 fi 217fi 218 219# if Gradle tasks are specified, then determine the appropriate shell command 220if [ "$gradleCommand" != "" ]; then 221 gradleCommand="$(echo "$gradleCommand" | sed 's/gradlew/gradlew --no-daemon/')" 222 # determine whether we can reduce the list of tasks we'll be running 223 # prepare directory 224 allTasksWork="$tempDir/allTasksWork" 225 allTasks="$tempDir/tasks" 226 if [ -e "$allTasks" ]; then 227 echo Skipping recalculating list of all relevant tasks, "$allTasks" already exists 228 else 229 echo Calculating list of tasks to run 230 rm -rf "$allTasksWork" 231 cp -r "$referenceFailingDir" "$allTasksWork" 232 # list tasks required for running this 233 if bash -c "cd $allTasksWork && OUT_DIR=out ./gradlew --no-daemon --dry-run $gradleTasks >log 2>&1"; then 234 echo "Expanded full list of tasks to run" 235 else 236 echo "Failed to expand full list of tasks to run; using given list of taks" 237 fi 238 # process output and split into files 239 mkdir -p "$allTasks" 240 taskListFile="$allTasksWork/tasklist" 241 # A task line will start with one or more project names separated by ":", then have a task name, and lastly either end of line or " " followed by a status (like UP-TO-DATE) 242 # We want the task path so we search for task lines and remove any trailing status 243 cat "$allTasksWork/log" | grep '^\(:[a-zA-Z0-9\-]\+\)\+\( \|$\)' | sed 's/ .*//' > "$taskListFile" 244 bash -c "cd $allTasks && split -l 1 '$taskListFile'" 245 # also include the original tasks in case either we failed to compute the list of tasks (due to the build failing during project configuration) or there are too many tasks to fit in one command line invocation 246 bash -c "cd $allTasks && echo '$gradleTasks' > givenTasks" 247 fi 248 249 # build command for passing to diff-filterer 250 # set OUT_DIR 251 testCommand="export OUT_DIR=out" 252 # delete log 253 testCommand="$testCommand && rm -f log" 254 # make sure at least one task exists 255 testCommand="$testCommand && ls tasks/* >/dev/null" 256 # build a shell script for running each task listed in the tasks/ dir 257 # We call xargs because the full set of tasks might be too long for the shell, and xargs will 258 # split into multiple gradlew invocations if needed 259 # Also, once we reproduce the error, we stop running more Gradle commands 260 testCommand="$testCommand && echo > run.sh && cat tasks/* | xargs echo '$grepCommand && exit 0; $gradleCommand' >> run.sh" 261 262 # run Gradle 263 testCommand="$testCommand && chmod u+x run.sh && ./run.sh >log 2>&1" 264 if [ "$clean" != "true" ]; then 265 # If the daemon is enabled, then sleep for a little bit in case Gradle fails very quickly 266 # If we run too many builds in a row with Gradle daemons enabled then the daemons might get confused 267 testCommand="$testCommand; sleep 2" 268 fi 269 # check for the error message that we want 270 testCommand="$testCommand; $grepCommand" 271 272 # identify a minimal set of tasks to reproduce the problem 273 minTasksFailing="$tempDir/minTasksFailing" 274 minTasksGoal="$referenceFailingDir" 275 minTasksOutput="$tempDir/minTasks_output" 276 if [ -e "$minTasksOutput" ]; then 277 echo already computed the minimum set of required tasks, can be seen in $minTasksGoal 278 else 279 rm -rf "$minTasksFailing" 280 cp -r "$minTasksGoal" "$minTasksFailing" 281 cp -r "$allTasks" "$minTasksFailing/" 282 echo Asking diff-filterer for a minimal set of tasks to reproduce this problem 283 if ./development/file-utils/diff-filterer.py --work-path "$tempDir" --num-jobs "$numJobs" "$minTasksFailing" "$minTasksGoal" "$testCommand"; then 284 echo diff-filterer successfully identifed a minimal set of required tasks 285 cp -r "$tempDir/bestResults" "$minTasksOutput" 286 else 287 failed 288 fi 289 fi 290 referenceFailingDir="$minTasksOutput" 291 echo Will use goal directory of "$referenceFailingDir" 292fi 293 294filtererStep1Work="$tempDir" 295filtererStep1Output="$filtererStep1Work/bestResults" 296fewestFilesOutputPath="$tempDir/fewestFiles" 297if echo "$resume" | grep "true" >/dev/null && stat "$fewestFilesOutputPath" >/dev/null 2>/dev/null; then 298 echo "Skipping asking diff-filterer for a minimal set of files, $fewestFilesOutputPath already exists" 299else 300 if [ "$resume" == "true" ]; then 301 if stat "$filtererStep1Output" >/dev/null 2>/dev/null; then 302 echo "Reusing $filtererStep1Output to resume asking diff-filterer for a minimal set of files" 303 # Copy the previous results to resume from 304 rm "$referenceFailingDir" -rf 305 cp -rT "$filtererStep1Output" "$referenceFailingDir" 306 else 307 echo "Cannot resume previous execution; neither $fewestFilesOutputPath nor $filtererStep1Output exists" 308 exit 1 309 fi 310 fi 311 echo Running diff-filterer.py once to identify the minimal set of files needed to reproduce the error 312 if ./development/file-utils/diff-filterer.py --work-path $filtererStep1Work --num-jobs "$numJobs" "$referenceFailingDir" "$referencePassingDir" "$testCommand"; then 313 echo diff-filterer completed successfully 314 else 315 failed 316 fi 317 echo Copying minimal set of files into $fewestFilesOutputPath 318 rm -rf "$fewestFilesOutputPath" 319 cp -rT "$filtererStep1Output" "$fewestFilesOutputPath" 320fi 321 322if [ "$subfilePath" == "" ]; then 323 echo Splitting files into individual lines was not enabled. Done. See results at $filtererStep1Work/bestResults 324else 325 if [ "$subfilePath" == "." ]; then 326 subfilePath="" 327 fi 328 if echo "$resume" | grep true >/dev/null && stat $fewestFilesOutputPath >/dev/null 2>/dev/null; then 329 echo "Skipping recopying $filtererStep1Output to $fewestFilesOutputPath" 330 else 331 echo Copying minimal set of files into $fewestFilesOutputPath 332 rm -rf "$fewestFilesOutputPath" 333 cp -rT "$filtererStep1Output" "$fewestFilesOutputPath" 334 fi 335 336 echo Creating working directory for identifying individually smallest files 337 noFunctionBodies_Passing="$tempDir/noFunctionBodies_Passing" 338 noFunctionBodies_goal="$tempDir/noFunctionBodies_goal" 339 noFunctionBodies_work="work" 340 noFunctionBodies_sandbox="$noFunctionBodies_work/$subfilePath" 341 noFunctionBodies_output="$tempDir/noFunctionBodies_output" 342 343 # set up command for running diff-filterer against diffs within files 344 filtererOptions="--num-jobs $numJobs" 345 346 if echo "$resume" | grep true >/dev/null && stat "$noFunctionBodies_output" >/dev/null 2>/dev/null; then 347 echo "Skipping asking diff-filterer to remove function bodies because $noFunctionBodies_output already exists" 348 else 349 echo Splitting files into smaller pieces 350 rm -rf "$noFunctionBodies_Passing" "$noFunctionBodies_goal" 351 mkdir -p "$noFunctionBodies_Passing" "$noFunctionBodies_goal" 352 cd "$noFunctionBodies_Passing" 353 cp -rT "$fewestFilesOutputPath" "$noFunctionBodies_work" 354 cp -rT "$noFunctionBodies_Passing" "$noFunctionBodies_goal" 355 356 splitsPath="${subfilePath}.split" 357 "${scriptPath}/impl/split.sh" --consolidate-leaves "$noFunctionBodies_sandbox" "$splitsPath" 358 rm "$noFunctionBodies_sandbox" -rf 359 360 echo Removing deepest lines 361 cd "$noFunctionBodies_goal" 362 "${scriptPath}/impl/split.sh" --remove-leaves "$noFunctionBodies_sandbox" "$splitsPath" 363 rm "$noFunctionBodies_sandbox" -rf 364 365 # restore any special files that we don't want to shrink 366 for path in $pathsToNotShrink; do 367 relativePathFromSubfilePath="$(realpath --relative-to="./$subfilePath" "$path")" 368 if ! echo "$relativePathFromSubfilePath" | grep "^\.\." >/dev/null 2>/dev/null; then 369 # This file is contained in $subfilePath so we were going to try to shrink it if it weren't exempt 370 echo Exempting "$path" from shrinking 371 # Copy the exploded version of the file that doesn't have any missing pieces 372 rm -rf "$splitsPath/$relativePathFromSubfilePath" 373 cp -rT "$noFunctionBodies_Passing/$splitsPath/$relativePathFromSubfilePath" "$noFunctionBodies_goal/$splitsPath/$relativePathFromSubfilePath" 374 fi 375 done 376 377 # 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 378 # 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 379 echo Running diff-filterer.py again to identify which function bodies can be removed 380 if "$supportRoot/development/file-utils/diff-filterer.py" $filtererOptions --allow-goal-passing --work-path "$(cd $supportRoot/../.. && pwd)" "$noFunctionBodies_Passing" "$noFunctionBodies_goal" "${scriptPath}/impl/join.sh ${splitsPath} ${noFunctionBodies_sandbox} && cd ${noFunctionBodies_work} && $testCommand"; then 381 echo diff-filterer completed successfully 382 else 383 failed 384 fi 385 386 echo Re-joining the files 387 rm -rf "${noFunctionBodies_output}" 388 cp -rT "$(cd $supportRoot/../../bestResults && pwd)" "${noFunctionBodies_output}" 389 cd "${noFunctionBodies_output}" 390 "${scriptPath}/impl/join.sh" "${splitsPath}" "${noFunctionBodies_sandbox}" 391 fi 392 393 # prepare for another invocation of diff-filterer, to remove other code that is now unused 394 smallestFilesInput="$tempDir/smallestFilesInput" 395 smallestFilesGoal="$tempDir/smallestFilesGoal" 396 smallestFilesWork="work" 397 smallestFilesSandbox="$smallestFilesWork/$subfilePath" 398 399 rm -rf "$smallestFilesInput" "$smallestFilesGoal" 400 mkdir -p "$smallestFilesInput" 401 cp -rT "${noFunctionBodies_output}" "$smallestFilesInput" 402 403 echo Splitting files into individual lines 404 cd "$smallestFilesInput" 405 splitsPath="${subfilePath}.split" 406 "${scriptPath}/impl/split.sh" "$smallestFilesSandbox" "$splitsPath" 407 rm "$smallestFilesSandbox" -rf 408 409 # Make a dir holding the destination file state 410 if [ "$limitToPath" != "" ]; then 411 # The user said they were only interested in trying to delete files under a certain path 412 # So, our target state is the original state minus that path (and its descendants) 413 mkdir -p "$smallestFilesGoal" 414 cp -rT "$smallestFilesInput/$smallestFilesWork" "$smallestFilesGoal/$smallestFilesWork" 415 cd "$smallestFilesGoal/$smallestFilesWork" 416 rm "$limitToPath" -rf 417 cd - 418 else 419 # The user didn't request to limit the search to a specific path, so we mostly try to delete as many 420 # files as possible 421 mkdir -p "$smallestFilesGoal" 422 fi 423 echo now check $smallestFilesGoal 424 # Restore any special exempt files 425 cd "$smallestFilesGoal" 426 for path in $pathsToNotShrink; do 427 relativePathFromSubfilePath="$(realpath --relative-to="./$subfilePath" "$path")" 428 if ! echo "$relativePathFromSubfilePath" | grep "^\.\." >/dev/null 2>/dev/null; then 429 # This file is contained in $subfilePath so we were going to try to shrink it if it weren't exempt 430 # Copy the exploded version of the file that doesn't have any missing pieces 431 echo Exempting "$path" from shrinking 432 destPath="$smallestFilesGoal/$splitsPath/$relativePathFromSubfilePath" 433 rm -rf "$destPath" 434 mkdir -p "$(dirname "$destPath")" 435 echo cp -rT "$noFunctionBodies_Passing/$splitsPath/$relativePathFromSubfilePath" "$destPath" 436 cp -rT "$noFunctionBodies_Passing/$splitsPath/$relativePathFromSubfilePath" "$smallestFilesGoal/$splitsPath/$relativePathFromSubfilePath" 437 fi 438 done 439 440 echo Running diff-filterer.py again to identify the minimal set of lines needed to reproduce the error 441 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 442 echo diff-filterer completed successfully 443 else 444 failed 445 fi 446 447 echo Re-joining the files 448 smallestFilesOutput="$tempDir/smallestFilesOutput" 449 rm -rf "$smallestFilesOutput" 450 cp -rT "$(cd $supportRoot/../../bestResults && pwd)" "${smallestFilesOutput}" 451 cd "${smallestFilesOutput}" 452 "${scriptPath}/impl/join.sh" "${splitsPath}" "${smallestFilesSandbox}" 453 454 echo "Done. See simplest discovered reproduction test case at ${smallestFilesOutput}/${smallestFilesWork}" 455fi 456notify succeeded 457