Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 1 | #!/bin/bash |
| 2 | set -e |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 3 | |
| 4 | scriptName="$(basename $0)" |
| 5 | |
| 6 | function usage() { |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 7 | echo "NAME" |
| 8 | echo " diagnose-build-failure.sh" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 9 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 10 | echo "SYNOPSIS" |
| 11 | echo " ./development/diagnose-build-failure/diagnose-build-failure.sh [--message <message>] '<tasks>'" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 12 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 13 | echo "DESCRIPTION" |
| 14 | echo " Attempts to identify why "'`'"./gradlew <tasks>"'`'" fails" |
| 15 | echo |
| 16 | echo "OPTIONS" |
| 17 | echo "--message <message>" |
| 18 | echo " Replaces the requirement for "'`'"./gradlew <tasks>"'`'" to fail with the requirement that it produces the given message" |
| 19 | echo |
| 20 | echo "SAMPLE USAGE" |
Jeff Gaston | 6c1bd59 | 2021-04-29 12:53:47 -0400 | [diff] [blame] | 21 | echo " $0 assembleRelease # or any other arguments you would normally give to ./gradlew" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 22 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 23 | echo "OUTPUT" |
| 24 | echo " diagnose-build-failure will conclude one of the following:" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 25 | echo |
| 26 | echo " A) Some state saved in memory by the Gradle daemon is triggering an error" |
| 27 | echo " B) Your source files have been changed" |
Jeff Gaston | 61cef33 | 2020-12-22 11:23:09 -0500 | [diff] [blame] | 28 | echo " To (slowly) generate a simpler reproduction case, you can run simplify-build-failure.sh" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 29 | echo " C) Some file in the out/ dir is triggering an error" |
| 30 | echo " If this happens, $scriptName will identify which file(s) specifically" |
| 31 | echo " D) The build is nondeterministic and/or affected by timestamps" |
| 32 | echo " E) The build via gradlew actually passes" |
| 33 | exit 1 |
| 34 | } |
| 35 | |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 36 | expectedMessage="" |
| 37 | while true; do |
| 38 | if [ "$#" -lt 1 ]; then |
| 39 | usage |
| 40 | fi |
| 41 | arg="$1" |
| 42 | shift |
| 43 | if [ "$arg" == "--message" ]; then |
| 44 | expectedMessage="$1" |
| 45 | shift |
| 46 | continue |
| 47 | fi |
| 48 | gradleArgs="$arg" |
| 49 | break |
| 50 | done |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 51 | if [ "$gradleArgs" == "" ]; then |
| 52 | usage |
| 53 | fi |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 54 | # split Gradle arguments into options and tasks |
| 55 | gradleOptions="" |
| 56 | gradleTasks="" |
| 57 | for arg in $gradleArgs; do |
| 58 | if [[ "$arg" == "-*" ]]; then |
| 59 | gradleOptions="$gradleOptions $arg" |
| 60 | else |
| 61 | gradleTasks="$gradleTasks $arg" |
| 62 | fi |
| 63 | done |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 64 | |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 65 | if [ "$#" -gt 0 ]; then |
| 66 | echo "Unrecognized argument: $1" |
| 67 | exit 1 |
| 68 | fi |
| 69 | |
Jeff Gaston | 6323450 | 2019-07-09 13:47:31 -0400 | [diff] [blame] | 70 | workingDir="$(pwd)" |
| 71 | if [ ! -e "$workingDir/gradlew" ]; then |
| 72 | echo "Error; ./gradlew does not exist. Must cd to a dir containing a ./gradlew first" |
| 73 | # so that this script knows which gradlew to use (in frameworks/support or frameworks/support/ui) |
| 74 | exit 1 |
| 75 | fi |
| 76 | |
Jeff Gaston | 65c35b9 | 2021-05-11 12:20:45 -0400 | [diff] [blame] | 77 | # resolve some paths |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 78 | scriptPath="$(cd $(dirname $0) && pwd)" |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 79 | vgrep="$scriptPath/impl/vgrep.sh" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 80 | supportRoot="$(cd $scriptPath/../.. && pwd)" |
| 81 | checkoutRoot="$(cd $supportRoot/../.. && pwd)" |
Jeff Gaston | a58e308 | 2019-08-05 19:44:26 -0400 | [diff] [blame] | 82 | tempDir="$checkoutRoot/diagnose-build-failure/" |
Jeff Gaston | 65c35b9 | 2021-05-11 12:20:45 -0400 | [diff] [blame] | 83 | if [ "$OUT_DIR" != "" ]; then |
| 84 | mkdir -p "$OUT_DIR" |
| 85 | OUT_DIR="$(cd $OUT_DIR && pwd)" |
| 86 | fi |
| 87 | if [ "$DIST_DIR" != "" ]; then |
| 88 | mkdir -p "$DIST_DIR" |
| 89 | DIST_DIR="$(cd $DIST_DIR && pwd)" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 90 | fi |
| 91 | COLOR_WHITE="\e[97m" |
| 92 | COLOR_GREEN="\e[32m" |
| 93 | |
| 94 | function checkStatusRepo() { |
| 95 | repo status |
| 96 | } |
| 97 | |
| 98 | function checkStatusGit() { |
| 99 | git status |
| 100 | git log -1 |
| 101 | } |
| 102 | |
| 103 | function checkStatus() { |
| 104 | cd "$checkoutRoot" |
| 105 | if [ "-e" .repo ]; then |
| 106 | checkStatusRepo |
| 107 | else |
| 108 | checkStatusGit |
| 109 | fi |
| 110 | } |
| 111 | |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 112 | # echos a shell command for running the build in the current directory |
Jeff Gaston | ec553a3 | 2020-09-03 10:55:44 -0400 | [diff] [blame] | 113 | function getBuildCommand() { |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 114 | if [ "$expectedMessage" == "" ]; then |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 115 | testCommand="$*" |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 116 | else |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 117 | testCommand="$* 2>&1 | $vgrep '$expectedMessage'" |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 118 | fi |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 119 | echo "$testCommand" |
| 120 | } |
| 121 | |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 122 | # Echos a shell command for testing the state in the current directory |
| 123 | # Status can be inverted by the '--invert' flag |
| 124 | # The dir of the state being tested is $testDir |
| 125 | # The dir of the source code is $workingDir |
| 126 | function getTestStateCommand() { |
| 127 | successStatus=0 |
| 128 | failureStatus=1 |
| 129 | if [[ "$1" == "--invert" ]]; then |
| 130 | successStatus=1 |
| 131 | failureStatus=0 |
| 132 | shift |
| 133 | fi |
| 134 | |
| 135 | setupCommand="testDir=\$(pwd) |
| 136 | $scriptPath/impl/restore-state.sh . $workingDir --move && cd $workingDir |
| 137 | " |
| 138 | buildCommand="$*" |
| 139 | cleanupCommand="$scriptPath/impl/backup-state.sh \$testDir $workingDir --move >/dev/null" |
| 140 | |
| 141 | fullFiltererCommand="$setupCommand |
Jeff Gaston | 735b125 | 2021-06-24 14:26:40 -0400 | [diff] [blame^] | 142 | if $buildCommand >/dev/null 2>/dev/null; then |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 143 | $cleanupCommand |
| 144 | exit $successStatus |
| 145 | else |
| 146 | $cleanupCommand |
| 147 | exit $failureStatus |
| 148 | fi" |
| 149 | |
| 150 | echo "$fullFiltererCommand" |
| 151 | } |
| 152 | |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 153 | function runBuild() { |
| 154 | testCommand="$(getBuildCommand $*)" |
Jeff Gaston | 40660e7 | 2020-01-21 16:46:14 -0500 | [diff] [blame] | 155 | cd "$workingDir" |
Jeff Gaston | 65c35b9 | 2021-05-11 12:20:45 -0400 | [diff] [blame] | 156 | echo Running $testCommand |
| 157 | if bash -c "$testCommand"; then |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 158 | echo -e "$COLOR_WHITE" |
| 159 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 160 | echo '`'$testCommand'`' succeeded |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 161 | return 0 |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 162 | else |
| 163 | echo -e "$COLOR_WHITE" |
| 164 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 165 | echo '`'$testCommand'`' failed |
Jeff Gaston | 599b9e3 | 2020-08-05 18:36:56 -0400 | [diff] [blame] | 166 | return 1 |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 167 | fi |
| 168 | } |
| 169 | |
| 170 | function backupState() { |
| 171 | cd "$scriptPath" |
| 172 | backupDir="$1" |
Jeff Gaston | 65c35b9 | 2021-05-11 12:20:45 -0400 | [diff] [blame] | 173 | shift |
| 174 | ./impl/backup-state.sh "$backupDir" "$workingDir" "$@" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 175 | } |
| 176 | |
| 177 | function restoreState() { |
| 178 | cd "$scriptPath" |
| 179 | backupDir="$1" |
Jeff Gaston | 6323450 | 2019-07-09 13:47:31 -0400 | [diff] [blame] | 180 | ./impl/restore-state.sh "$backupDir" "$workingDir" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 181 | } |
| 182 | |
| 183 | function clearState() { |
| 184 | restoreState /dev/null |
| 185 | } |
| 186 | |
| 187 | echo |
| 188 | echo "Making sure that we can reproduce the build failure" |
| 189 | if runBuild ./gradlew $gradleArgs; then |
| 190 | echo |
| 191 | echo "This script failed to reproduce the build failure." |
| 192 | echo "If the build failure you were observing was in Android Studio, then:" |
| 193 | echo ' Were you launching Android Studio by running `./studiow`?' |
| 194 | echo " Try asking a team member why Android Studio is failing but gradlew is succeeding" |
| 195 | echo "If you previously observed a build failure, then this means one of:" |
| 196 | echo " The state of your build is different than when you started your previous build" |
| 197 | echo " You could ask a team member if they've seen this error." |
| 198 | echo " The build is nondeterministic" |
| 199 | echo " If this seems likely to you, then please open a bug." |
| 200 | exit 1 |
| 201 | else |
| 202 | echo |
| 203 | echo "Reproduced build failure" |
| 204 | fi |
| 205 | |
| 206 | echo |
| 207 | echo "Stopping the Gradle Daemon and rebuilding" |
| 208 | cd "$supportRoot" |
| 209 | ./gradlew --stop || true |
| 210 | if runBuild ./gradlew --no-daemon $gradleArgs; then |
| 211 | echo |
| 212 | echo "The build passed when disabling the Gradle Daemon" |
| 213 | echo "This suggests that there is some state saved in the Gradle Daemon that is causing a failure." |
| 214 | echo "Unfortunately, this script does not know how to diagnose this further." |
| 215 | echo "You could ask a team member if they've seen this error." |
| 216 | exit 1 |
| 217 | else |
| 218 | echo |
| 219 | echo "The build failed even with the Gradle Daemon disabled." |
| 220 | echo "This may mean that there is state stored in a file somewhere, triggering the build to fail." |
| 221 | echo "We will investigate the possibility of saved state next." |
| 222 | echo |
Jeff Gaston | a1280e0 | 2021-04-16 16:43:02 -0400 | [diff] [blame] | 223 | # We're going to immediately overwrite the user's current state, |
| 224 | # so we can simply move the current state into $tempDir/prev rather than copying it |
| 225 | backupState "$tempDir/prev" --move |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 226 | fi |
| 227 | |
| 228 | echo |
| 229 | echo "Checking whether a clean build passes" |
| 230 | clearState |
| 231 | backupState "$tempDir/empty" |
| 232 | successState="$tempDir/empty" |
| 233 | if runBuild ./gradlew --no-daemon $gradleArgs; then |
| 234 | echo |
| 235 | echo "The clean build passed, so we can now investigate what cached state is triggering this build to fail." |
| 236 | backupState "$tempDir/clean" |
| 237 | else |
| 238 | echo |
Jeff Gaston | a6c66504 | 2020-07-22 12:57:33 -0400 | [diff] [blame] | 239 | echo "The clean build also reproduced the issue." |
| 240 | echo "This may mean that everyone is observing this issue" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 241 | echo "This may mean that something about your checkout is different from others'" |
Jeff Gaston | 216c970 | 2019-05-14 17:44:16 -0400 | [diff] [blame] | 242 | echo "You may be interested in running development/simplify-build-failure/simplify-build-failure.sh to identify the minimal set of source files required to reproduce this error" |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 243 | echo "Checking the status of your checkout:" |
| 244 | checkStatus |
| 245 | exit 1 |
| 246 | fi |
| 247 | |
| 248 | echo |
| 249 | echo "Checking whether a second build passes when starting from the output of the first clean build" |
| 250 | if runBuild ./gradlew --no-daemon $gradleArgs; then |
| 251 | echo |
| 252 | echo "The next build after the clean build passed, so we can use the output of the first clean build as the successful state to compare against" |
| 253 | successState="$tempDir/clean" |
| 254 | else |
| 255 | echo |
| 256 | echo "The next build after the clean build failed." |
| 257 | echo "Although this is unexpected, we should still be able to diagnose it." |
| 258 | echo "This might be slower than normal, though, because it may require us to rebuild more things more often" |
| 259 | fi |
| 260 | |
| 261 | echo |
| 262 | echo "Next we'll double-check that after restoring the failing state, the build fails" |
| 263 | restoreState "$tempDir/prev" |
| 264 | if runBuild ./gradlew --no-daemon $gradleArgs; then |
| 265 | echo |
| 266 | echo "After restoring the saved state, the build passed." |
| 267 | echo "This might mean that there is additional state being saved somewhere else that this script does not know about" |
| 268 | echo "This might mean that the success or failure status of the build is dependent on timestamps." |
| 269 | echo "This might mean that the build is nondeterministic." |
| 270 | echo "Unfortunately, this script does not know how to diagnose this further." |
| 271 | echo "You could:" |
| 272 | echo " Ask a team member if they know where the state may be stored" |
| 273 | echo " Ask a team member if they recognize the build error" |
| 274 | exit 1 |
| 275 | else |
| 276 | echo |
| 277 | echo "After restoring the saved state, the build failed. This confirms that this script is successfully saving and restoring the relevant state" |
| 278 | fi |
| 279 | |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 280 | # Ask diff-filterer.py to run a binary search to determine the minimum set of tasks that must be passed to reproduce this error |
| 281 | # (it's possible that the caller passed more tasks than needed, particularly if the caller is a script) |
| 282 | requiredTasksDir="$tempDir/requiredTasks" |
| 283 | function determineMinimalSetOfRequiredTasks() { |
| 284 | echo Calculating the list of tasks to run |
| 285 | allTasksLog="$tempDir/tasks.log" |
| 286 | restoreState "$successState" |
| 287 | rm -f "$allTasksLog" |
| 288 | bash -c "cd $workingDir && ./gradlew --no-daemon --dry-run $gradleArgs > $allTasksLog 2>&1" || true |
| 289 | |
| 290 | # process output and split into files |
| 291 | taskListFile="$tempDir/tasks.list" |
| 292 | cat "$allTasksLog" | grep '^:' | sed 's/ .*//' > "$taskListFile" |
| 293 | requiredTasksWork="$tempDir/requiredTasksWork" |
| 294 | rm -rf "$requiredTasksWork" |
| 295 | cp -r "$tempDir/prev" "$requiredTasksWork" |
| 296 | mkdir -p "$requiredTasksWork/tasks" |
| 297 | bash -c "cd $requiredTasksWork/tasks && split -l 1 '$taskListFile'" |
| 298 | |
| 299 | rm -rf "$requiredTasksDir" |
| 300 | # Build the command for passing to diff-filterer. |
| 301 | # We call xargs because the full set of tasks might be too long for the shell, and xargs will |
| 302 | # split into multiple gradlew invocations if needed. |
| 303 | # We also cd into the tasks/ dir before calling 'cat' to avoid reaching its argument length limit. |
| 304 | # note that the variable "$testDir" gets set by $getTestStateCommand |
| 305 | buildCommand="$(getBuildCommand "rm -f log && (cd \$testDir/tasks && cat *) | xargs --no-run-if-empty ./gradlew $gradleOptions")" |
| 306 | |
| 307 | # command for moving state, running build, and moving state back |
| 308 | fullFiltererCommand="$(getTestStateCommand --invert $buildCommand)" |
| 309 | |
| 310 | if $supportRoot/development/file-utils/diff-filterer.py --work-path "$tempDir" "$requiredTasksWork" "$tempDir/prev" "$fullFiltererCommand"; then |
| 311 | echo diff-filterer successfully identified a minimal set of required tasks. Saving into $requiredTasksDir |
| 312 | cp -r "$tempDir/bestResults/tasks" "$requiredTasksDir" |
| 313 | else |
| 314 | echo diff-filterer was unable to identify a minimal set of tasks required to reproduce the error |
| 315 | exit 1 |
| 316 | fi |
| 317 | } |
| 318 | determineMinimalSetOfRequiredTasks |
| 319 | # update variables |
| 320 | gradleTasks="$(cat $requiredTasksDir/*)" |
| 321 | gradleArgs="$gradleOptions $gradleTasks" |
| 322 | |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 323 | # Now ask diff-filterer.py to run a binary search to determine what the relevant differences are between "$tempDir/prev" and "$tempDir/clean" |
| 324 | echo |
| 325 | echo "Binary-searching the contents of the two output directories until the relevant differences are identified." |
| 326 | echo "This may take a while." |
| 327 | echo |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 328 | |
| 329 | # command for running a build |
Jeff Gaston | 5725bdc | 2021-05-13 17:53:20 -0400 | [diff] [blame] | 330 | buildCommand="$(getBuildCommand "./gradlew --no-daemon $gradleArgs")" |
Jeff Gaston | f1817f7 | 2021-06-07 15:28:42 -0400 | [diff] [blame] | 331 | # command for moving state, running build, and moving state back |
| 332 | fullFiltererCommand="$(getTestStateCommand $buildCommand)" |
| 333 | |
Jeff Gaston | 5725bdc | 2021-05-13 17:53:20 -0400 | [diff] [blame] | 334 | if $supportRoot/development/file-utils/diff-filterer.py --assume-input-states-are-correct --work-path $tempDir $successState $tempDir/prev "$fullFiltererCommand"; then |
Jeff Gaston | cc0993d | 2019-04-02 18:02:44 -0400 | [diff] [blame] | 335 | echo |
| 336 | echo "There should be something wrong with the above file state" |
| 337 | echo "Hopefully the output from diff-filterer.py above is enough information for you to figure out what is wrong" |
| 338 | echo "If not, you could ask a team member about your original error message and see if they have any ideas" |
| 339 | else |
| 340 | echo |
| 341 | echo "Something went wrong running diff-filterer.py" |
| 342 | echo "Maybe that means the build is nondeterministic" |
| 343 | echo "Maybe that means that there's something wrong with this script ($0)" |
| 344 | fi |