blob: ebaf88d1504481823976e34718bb38ad25c67f77 [file] [log] [blame]
Jeff Gastoncc0993d2019-04-02 18:02:44 -04001#!/bin/bash
2set -e
Jeff Gastoncc0993d2019-04-02 18:02:44 -04003
4scriptName="$(basename $0)"
5
6function usage() {
Jeff Gastona6c665042020-07-22 12:57:33 -04007 echo "NAME"
8 echo " diagnose-build-failure.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -04009 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040010 echo "SYNOPSIS"
Jeff Gaston1ffc4852021-06-24 12:02:44 -040011 echo " ./development/diagnose-build-failure/diagnose-build-failure.sh [--message <message>] [--timeout <seconds> ] '<tasks>'"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040012 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040013 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 Gaston6c1bd592021-04-29 12:53:47 -040021 echo " $0 assembleRelease # or any other arguments you would normally give to ./gradlew"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040022 echo
Jeff Gastona6c665042020-07-22 12:57:33 -040023 echo "OUTPUT"
24 echo " diagnose-build-failure will conclude one of the following:"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040025 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 Gaston61cef332020-12-22 11:23:09 -050028 echo " To (slowly) generate a simpler reproduction case, you can run simplify-build-failure.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040029 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 Gastona6c665042020-07-22 12:57:33 -040036expectedMessage=""
Jeff Gaston1ffc4852021-06-24 12:02:44 -040037timeoutSeconds=""
Jeff Gastona6c665042020-07-22 12:57:33 -040038while true; do
39 if [ "$#" -lt 1 ]; then
40 usage
41 fi
42 arg="$1"
43 shift
44 if [ "$arg" == "--message" ]; then
45 expectedMessage="$1"
46 shift
47 continue
48 fi
Jeff Gaston1ffc4852021-06-24 12:02:44 -040049 if [ "$arg" == "--timeout" ]; then
50 timeoutSeconds="$1"
51 shift
52 continue
53 fi
54
Jeff Gastona6c665042020-07-22 12:57:33 -040055 gradleArgs="$arg"
56 break
57done
Jeff Gastoncc0993d2019-04-02 18:02:44 -040058if [ "$gradleArgs" == "" ]; then
59 usage
60fi
Jeff Gaston1ffc4852021-06-24 12:02:44 -040061if [ "$timeoutSeconds" == "" ]; then
62 timeoutArg=""
63else
64 timeoutArg="--timeout $timeoutSeconds"
65fi
Jeff Gastonf1817f72021-06-07 15:28:42 -040066# split Gradle arguments into options and tasks
67gradleOptions=""
68gradleTasks=""
69for arg in $gradleArgs; do
70 if [[ "$arg" == "-*" ]]; then
71 gradleOptions="$gradleOptions $arg"
72 else
73 gradleTasks="$gradleTasks $arg"
74 fi
75done
Jeff Gastoncc0993d2019-04-02 18:02:44 -040076
Jeff Gastona6c665042020-07-22 12:57:33 -040077if [ "$#" -gt 0 ]; then
78 echo "Unrecognized argument: $1"
79 exit 1
80fi
81
Jeff Gaston63234502019-07-09 13:47:31 -040082workingDir="$(pwd)"
83if [ ! -e "$workingDir/gradlew" ]; then
84 echo "Error; ./gradlew does not exist. Must cd to a dir containing a ./gradlew first"
85 # so that this script knows which gradlew to use (in frameworks/support or frameworks/support/ui)
86 exit 1
87fi
88
Jeff Gaston65c35b92021-05-11 12:20:45 -040089# resolve some paths
Jeff Gastoncc0993d2019-04-02 18:02:44 -040090scriptPath="$(cd $(dirname $0) && pwd)"
Jeff Gaston599b9e32020-08-05 18:36:56 -040091vgrep="$scriptPath/impl/vgrep.sh"
Jeff Gastoncc0993d2019-04-02 18:02:44 -040092supportRoot="$(cd $scriptPath/../.. && pwd)"
93checkoutRoot="$(cd $supportRoot/../.. && pwd)"
Jeff Gastona58e3082019-08-05 19:44:26 -040094tempDir="$checkoutRoot/diagnose-build-failure/"
Jeff Gaston65c35b92021-05-11 12:20:45 -040095if [ "$OUT_DIR" != "" ]; then
96 mkdir -p "$OUT_DIR"
97 OUT_DIR="$(cd $OUT_DIR && pwd)"
Jeff Gastonbaa4ca52021-07-20 15:50:39 -040098 EFFECTIVE_OUT_DIR="$OUT_DIR"
99else
100 EFFECTIVE_OUT_DIR="$checkoutRoot/out"
Jeff Gaston65c35b92021-05-11 12:20:45 -0400101fi
102if [ "$DIST_DIR" != "" ]; then
103 mkdir -p "$DIST_DIR"
104 DIST_DIR="$(cd $DIST_DIR && pwd)"
Jeff Gastonbaa4ca52021-07-20 15:50:39 -0400105 EFFECTIVE_DIST_DIR=$DIST_DIR
106else
107 # If $DIST_DIR was unset, we leave it unset just in case setting it could affect the build
108 # However, we still need to keep track of where the files are going to go, so
109 # we set EFFECTIVE_DIST_DIR
110 EFFECTIVE_DIST_DIR="$EFFECTIVE_OUT_DIR/dist"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400111fi
112COLOR_WHITE="\e[97m"
113COLOR_GREEN="\e[32m"
114
115function checkStatusRepo() {
116 repo status
117}
118
119function checkStatusGit() {
120 git status
121 git log -1
122}
123
124function checkStatus() {
125 cd "$checkoutRoot"
126 if [ "-e" .repo ]; then
127 checkStatusRepo
128 else
129 checkStatusGit
130 fi
131}
132
Jeff Gastonf1817f72021-06-07 15:28:42 -0400133# echos a shell command for running the build in the current directory
Jeff Gastonec553a32020-09-03 10:55:44 -0400134function getBuildCommand() {
Jeff Gastona6c665042020-07-22 12:57:33 -0400135 if [ "$expectedMessage" == "" ]; then
Jeff Gastona6c665042020-07-22 12:57:33 -0400136 testCommand="$*"
Jeff Gastona6c665042020-07-22 12:57:33 -0400137 else
Jeff Gaston599b9e32020-08-05 18:36:56 -0400138 testCommand="$* 2>&1 | $vgrep '$expectedMessage'"
Jeff Gastona6c665042020-07-22 12:57:33 -0400139 fi
Jeff Gaston599b9e32020-08-05 18:36:56 -0400140 echo "$testCommand"
141}
142
Jeff Gastonf1817f72021-06-07 15:28:42 -0400143# Echos a shell command for testing the state in the current directory
144# Status can be inverted by the '--invert' flag
145# The dir of the state being tested is $testDir
146# The dir of the source code is $workingDir
147function getTestStateCommand() {
148 successStatus=0
149 failureStatus=1
150 if [[ "$1" == "--invert" ]]; then
151 successStatus=1
152 failureStatus=0
153 shift
154 fi
155
156 setupCommand="testDir=\$(pwd)
157$scriptPath/impl/restore-state.sh . $workingDir --move && cd $workingDir
158"
159 buildCommand="$*"
160 cleanupCommand="$scriptPath/impl/backup-state.sh \$testDir $workingDir --move >/dev/null"
161
162 fullFiltererCommand="$setupCommand
Jeff Gaston735b1252021-06-24 14:26:40 -0400163if $buildCommand >/dev/null 2>/dev/null; then
Jeff Gastonf1817f72021-06-07 15:28:42 -0400164 $cleanupCommand
165 exit $successStatus
166else
167 $cleanupCommand
168 exit $failureStatus
169fi"
170
171 echo "$fullFiltererCommand"
172}
173
Jeff Gaston599b9e32020-08-05 18:36:56 -0400174function runBuild() {
175 testCommand="$(getBuildCommand $*)"
Jeff Gaston40660e72020-01-21 16:46:14 -0500176 cd "$workingDir"
Jeff Gaston65c35b92021-05-11 12:20:45 -0400177 echo Running $testCommand
178 if bash -c "$testCommand"; then
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400179 echo -e "$COLOR_WHITE"
180 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400181 echo '`'$testCommand'`' succeeded
Jeff Gaston599b9e32020-08-05 18:36:56 -0400182 return 0
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400183 else
184 echo -e "$COLOR_WHITE"
185 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400186 echo '`'$testCommand'`' failed
Jeff Gaston599b9e32020-08-05 18:36:56 -0400187 return 1
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400188 fi
189}
190
191function backupState() {
192 cd "$scriptPath"
193 backupDir="$1"
Jeff Gaston65c35b92021-05-11 12:20:45 -0400194 shift
195 ./impl/backup-state.sh "$backupDir" "$workingDir" "$@"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400196}
197
198function restoreState() {
199 cd "$scriptPath"
200 backupDir="$1"
Jeff Gaston63234502019-07-09 13:47:31 -0400201 ./impl/restore-state.sh "$backupDir" "$workingDir"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400202}
203
204function clearState() {
205 restoreState /dev/null
206}
207
208echo
209echo "Making sure that we can reproduce the build failure"
Jeff Gastonbaa4ca52021-07-20 15:50:39 -0400210if runBuild ./gradlew -Pandroidx.summarizeStderr $gradleArgs; then
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400211 echo
212 echo "This script failed to reproduce the build failure."
213 echo "If the build failure you were observing was in Android Studio, then:"
214 echo ' Were you launching Android Studio by running `./studiow`?'
215 echo " Try asking a team member why Android Studio is failing but gradlew is succeeding"
216 echo "If you previously observed a build failure, then this means one of:"
217 echo " The state of your build is different than when you started your previous build"
218 echo " You could ask a team member if they've seen this error."
219 echo " The build is nondeterministic"
220 echo " If this seems likely to you, then please open a bug."
221 exit 1
222else
223 echo
224 echo "Reproduced build failure"
225fi
226
Jeff Gastonbaa4ca52021-07-20 15:50:39 -0400227if [ "$expectedMessage" == "" ]; then
228 summaryLog="$EFFECTIVE_DIST_DIR/logs/error_summary.log"
229 echo
230 echo "No failure message specified. Computing appropriate failure message from $summaryLog"
231 echo
232 longestLine="$(awk '{ if (length($0) > maxLength) {maxLength = length($0); longestLine = $0} } END { print longestLine }' $summaryLog)"
233 echo "Longest line:"
234 echo
235 echo "$longestLine"
236 echo
237 if grep "$longestLine" "$summaryLog" >/dev/null 2>/dev/null; then
238 echo "We will use this as the message to test for"
239 echo
240 expectedMessage="$longestLine"
241 else
242 echo "The identified line could not be found in the summary log via grep. Is it possible that diagnose-build-failure did not correctly escape the message?"
243 exit 1
244 fi
245fi
246
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400247echo
248echo "Stopping the Gradle Daemon and rebuilding"
249cd "$supportRoot"
250./gradlew --stop || true
251if runBuild ./gradlew --no-daemon $gradleArgs; then
252 echo
253 echo "The build passed when disabling the Gradle Daemon"
254 echo "This suggests that there is some state saved in the Gradle Daemon that is causing a failure."
255 echo "Unfortunately, this script does not know how to diagnose this further."
256 echo "You could ask a team member if they've seen this error."
257 exit 1
258else
259 echo
260 echo "The build failed even with the Gradle Daemon disabled."
261 echo "This may mean that there is state stored in a file somewhere, triggering the build to fail."
262 echo "We will investigate the possibility of saved state next."
263 echo
Jeff Gastona1280e02021-04-16 16:43:02 -0400264 # We're going to immediately overwrite the user's current state,
265 # so we can simply move the current state into $tempDir/prev rather than copying it
266 backupState "$tempDir/prev" --move
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400267fi
268
269echo
270echo "Checking whether a clean build passes"
271clearState
272backupState "$tempDir/empty"
273successState="$tempDir/empty"
274if runBuild ./gradlew --no-daemon $gradleArgs; then
275 echo
276 echo "The clean build passed, so we can now investigate what cached state is triggering this build to fail."
277 backupState "$tempDir/clean"
278else
279 echo
Jeff Gastona6c665042020-07-22 12:57:33 -0400280 echo "The clean build also reproduced the issue."
281 echo "This may mean that everyone is observing this issue"
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400282 echo "This may mean that something about your checkout is different from others'"
Jeff Gaston216c9702019-05-14 17:44:16 -0400283 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 Gastoncc0993d2019-04-02 18:02:44 -0400284 echo "Checking the status of your checkout:"
285 checkStatus
286 exit 1
287fi
288
289echo
290echo "Checking whether a second build passes when starting from the output of the first clean build"
291if runBuild ./gradlew --no-daemon $gradleArgs; then
292 echo
293 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"
294 successState="$tempDir/clean"
295else
296 echo
297 echo "The next build after the clean build failed."
298 echo "Although this is unexpected, we should still be able to diagnose it."
299 echo "This might be slower than normal, though, because it may require us to rebuild more things more often"
300fi
301
302echo
303echo "Next we'll double-check that after restoring the failing state, the build fails"
304restoreState "$tempDir/prev"
305if runBuild ./gradlew --no-daemon $gradleArgs; then
306 echo
307 echo "After restoring the saved state, the build passed."
308 echo "This might mean that there is additional state being saved somewhere else that this script does not know about"
309 echo "This might mean that the success or failure status of the build is dependent on timestamps."
310 echo "This might mean that the build is nondeterministic."
311 echo "Unfortunately, this script does not know how to diagnose this further."
312 echo "You could:"
313 echo " Ask a team member if they know where the state may be stored"
314 echo " Ask a team member if they recognize the build error"
315 exit 1
316else
317 echo
318 echo "After restoring the saved state, the build failed. This confirms that this script is successfully saving and restoring the relevant state"
319fi
320
Jeff Gastonf1817f72021-06-07 15:28:42 -0400321# Ask diff-filterer.py to run a binary search to determine the minimum set of tasks that must be passed to reproduce this error
322# (it's possible that the caller passed more tasks than needed, particularly if the caller is a script)
323requiredTasksDir="$tempDir/requiredTasks"
324function determineMinimalSetOfRequiredTasks() {
325 echo Calculating the list of tasks to run
326 allTasksLog="$tempDir/tasks.log"
327 restoreState "$successState"
328 rm -f "$allTasksLog"
329 bash -c "cd $workingDir && ./gradlew --no-daemon --dry-run $gradleArgs > $allTasksLog 2>&1" || true
330
331 # process output and split into files
332 taskListFile="$tempDir/tasks.list"
333 cat "$allTasksLog" | grep '^:' | sed 's/ .*//' > "$taskListFile"
334 requiredTasksWork="$tempDir/requiredTasksWork"
335 rm -rf "$requiredTasksWork"
336 cp -r "$tempDir/prev" "$requiredTasksWork"
337 mkdir -p "$requiredTasksWork/tasks"
338 bash -c "cd $requiredTasksWork/tasks && split -l 1 '$taskListFile'"
339
340 rm -rf "$requiredTasksDir"
341 # Build the command for passing to diff-filterer.
342 # We call xargs because the full set of tasks might be too long for the shell, and xargs will
343 # split into multiple gradlew invocations if needed.
344 # We also cd into the tasks/ dir before calling 'cat' to avoid reaching its argument length limit.
345 # note that the variable "$testDir" gets set by $getTestStateCommand
346 buildCommand="$(getBuildCommand "rm -f log && (cd \$testDir/tasks && cat *) | xargs --no-run-if-empty ./gradlew $gradleOptions")"
347
348 # command for moving state, running build, and moving state back
349 fullFiltererCommand="$(getTestStateCommand --invert $buildCommand)"
350
Jeff Gaston1ffc4852021-06-24 12:02:44 -0400351 if $supportRoot/development/file-utils/diff-filterer.py $timeoutArg --work-path "$tempDir" "$requiredTasksWork" "$tempDir/prev" "$fullFiltererCommand"; then
Jeff Gastonf1817f72021-06-07 15:28:42 -0400352 echo diff-filterer successfully identified a minimal set of required tasks. Saving into $requiredTasksDir
353 cp -r "$tempDir/bestResults/tasks" "$requiredTasksDir"
354 else
355 echo diff-filterer was unable to identify a minimal set of tasks required to reproduce the error
356 exit 1
357 fi
358}
359determineMinimalSetOfRequiredTasks
360# update variables
361gradleTasks="$(cat $requiredTasksDir/*)"
362gradleArgs="$gradleOptions $gradleTasks"
363
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400364# Now ask diff-filterer.py to run a binary search to determine what the relevant differences are between "$tempDir/prev" and "$tempDir/clean"
365echo
366echo "Binary-searching the contents of the two output directories until the relevant differences are identified."
367echo "This may take a while."
368echo
Jeff Gastonf1817f72021-06-07 15:28:42 -0400369
370# command for running a build
Jeff Gaston5725bdc2021-05-13 17:53:20 -0400371buildCommand="$(getBuildCommand "./gradlew --no-daemon $gradleArgs")"
Jeff Gastonf1817f72021-06-07 15:28:42 -0400372# command for moving state, running build, and moving state back
373fullFiltererCommand="$(getTestStateCommand $buildCommand)"
374
Jeff Gaston1ffc4852021-06-24 12:02:44 -0400375if $supportRoot/development/file-utils/diff-filterer.py $timeoutArg --assume-input-states-are-correct --work-path $tempDir $successState $tempDir/prev "$fullFiltererCommand"; then
Jeff Gastoncc0993d2019-04-02 18:02:44 -0400376 echo
377 echo "There should be something wrong with the above file state"
378 echo "Hopefully the output from diff-filterer.py above is enough information for you to figure out what is wrong"
379 echo "If not, you could ask a team member about your original error message and see if they have any ideas"
380else
381 echo
382 echo "Something went wrong running diff-filterer.py"
383 echo "Maybe that means the build is nondeterministic"
384 echo "Maybe that means that there's something wrong with this script ($0)"
385fi